[ARFC] Onboard tETH to Aave v3 Prime Instance

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
: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

  • 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 :yellow_circle:
owner: Safe 5-of-7 transferOwnership, updateBlacklister HIGH :green_circle:
not assigned blacklist, unBlacklist HIGH :green_circle:
TreehouseRouter, TreehouseAccounting, TreehouseRedemptionV2, TreehouseFastlane deposit, mint, withdraw, redeem HIGH :green_circle:
  • 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 the mint(amount, to) function.
    • Allowed contract can redeem tETH via the redeem(amount, to, from) function. An alternative is the withdraw(amount, to, from) function.
  • Exchange Rate
    • As an ERC4626, tETH uses the internal convertToAssets(shares) function to calculate the current tETH exchange rate.

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 :yellow_circle:
TreehouseRouter, TreehouseAccounting, TreehouseRedemptionV2, TreehouseFastlane burn, burnFrom, mintTo, HIGH :green_circle:
  • 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) and removeMinter(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.

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 :green_circle:
not assigned rescueETH, rescueERC20 HIGH :green_circle:
  • 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 to 100000 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) and rescueETH(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.

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 :yellow_circle:
TreehouseRedemptionV2, TreehouseFastlane redeem HIGH :green_circle:
owner: Safe 5-of-7, pauser: not assigned setPause HIGH :green_circle:
  • 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 the amount from the Vault contract to the recipient 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 :green_circle:
  • Access Control
    • The owner can set the minimum amount that can be redeemed instantly via the setMinRedeem(amount) function. Currently, it is set to 200 wstETH.
    • The owner can set the minimum waiting period via the setWaitingPeriod(secs) function. Currently, it is set to 7 days.
    • The owner can set the fee applied during the redemption by calling the setRedemptionFee(bps) function. Currently set to 0.05%.
  • 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 the tETH.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 the redemptionController.redeem(amount, user) function, transferring the wstETH to the user

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 :green_circle:
  • Access Control
    • The owner can set the minimum amount that can be redeemed instantly via the setMinRedeem(amount) function. Currently, it is set to 0.
    • 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%.
  • 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 the redemptionController.redeem(amount, user) function, transferring the wstETH to the user

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 :yellow_circle:
Safe 5-of-7, Safe 1-of-5 doAccounting HIGH :yellow_circle:
  • 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 to 0.025%.
    • The owner can pause/unpause the contract by calling the setPauser(bool) function.
  • 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 calls TreehouseAccounting.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 the token.balanceOf(vault) function. This approach could potentially expose the system to donation attacks. However, given the deviation variable in place, if the netPNL between the currentNav and lastNav is bigger than the lastNav * deviation, the exchange rate update reverts, acting as a safeguard from this kind of attack.

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 :yellow_circle:
Safe 5-of-7, PnlAccounting mark HIGH :yellow_circle:
  • 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, where type means to mint or burn IAU tokens. If it marks to mint, it mints the amount of IAU tokens, sends them to the tETH vault, and mints the fee value in tETH to the treasury. Otherwise, it just burns the amount of IAU tokens locked in the tETH vault.

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 :yellow_circle:
not assigned rescueETH, rescueERC20 HIGH :green_circle:
tETH Strategy withdraw HIGH :green_circle:
  • 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) and removeAllowableAsset(address) functions, respectively.
    • Active strategies in the StrategyStorage Contract can withdraw allowed assets via the withdraw(asset, amount) function.

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 :yellow_circle:
  • 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[]) and unwhitelistActions(id, actions[]) functions, respectively.
    • The owner can add and remove assets for different strategies via the whitelistAssets(id, assets[]) and unwhitelistAssets(id, assets[]) functions, respectively.
    • The owner can pause/unpause strategies via the pauseStrategy(id) and unpauseStrategy(id) functions.
    • The owner can update the StrategyExecutor address by calling the setStrategyExecutor(address) function.

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 :yellow_circle:
- executeOnStrategy⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ HIGH :green_circle:
  • 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 strategy id, list of actions, their calldata, and param mappings.
  • 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 calls strategy.callExecute() function which initiates the execution of the actions.

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 :green_circle:
  • Executing Actions
    • Only the StrategyExecutor contract initiates the actions on the Strategy contract via the callExecute(target, data) function. It passes as target the ActionsExecutor contract, which will perform delegate calls on each action contract via the actionExecutor.executeActions(actionsIds[], actionCalldata[], paramMapping[]) function.

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 the actionRegistry.getAddr(actionId), and then, via its internal delegateCallAndReturnBytes32(target, data), it performs a delegatecall (using the storage from the Strategy contract) by calling the action.executeAction(calldata) function.

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 :yellow_circle:
  • 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 action id 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 the id, set the final action address using the pending one, and keep the previous action address in the previousAddresses mapping if needed to roll back.
    • The owner can cancel the action contact change via the cancelContractChange(id), setting the pending address to address(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 action id address with the address stored in the previousAddresses[id] mapping.

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.

3 Likes