ERC 4626 Vault implementation

Hello,
I am studying the OpenZeppelin ERC4626 implementation so I can create a vault that derives from it and I had a few basic questions.

I do not see any code related to initialization of the ERC share token or vToken similar to what is present in the solmate implementation. Is the idea that this should actually happen in the vault smart contract that inherits from ERC4626.sol?

Also I have been looking at a few different ERC4626 vault implementations online and most of them have some sort of 'shareholder' mapping of user addresses to uint to track user deposits. I am wondering if this is really necessary given that vToken are issued for each deposit - isn't this sufficient to account for claims on the vault's underlying funds without having an explicit mapping? And wouldn't such a mapping make the contract brittle should the user trade / send the vTokens to someone else?

Thanks in advance,
Jonathan

1 Like

The initialization (deployment) of a token is usually executed in an independent context from the initialization (deployment) of an entity which interacts with that token.

The fundamental reason for this, is that the creator of a token does not have any pre-knowledge of every future entity which will choose to interact with that token.

And as you can see in the code:

constructor(IERC20 asset_) {
    (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
    _underlyingDecimals = success ? assetDecimals : 18;
    _asset = asset_;
}

When this contract is deployed, it receives a "pointer" to a presumably already-deployed token.

1 Like

Thanks so much for the reply.

I understand that the vault contract's constructor receives a pointer (address) to a pre-deployed ERC20 or ERC777 token, which is the underlying asset of the vault.

My question is more in regards to the vault token ('share') itself. This token, I assume would be initialized with the deployment of the vault. For example in the solmate implementation the constructor receives the vToken name and symbol:

constructor(
        ERC20 _asset,
        string memory _name,
        string memory _symbol
    ) ERC20(_name, _symbol, _asset.decimals()) {
        asset = _asset;
    }

Note that here 'asset' refers to the independently-existing, underlying token that would be deposited into the vault but 'name' and 'symbol' refer to the vault token (share), if my understanding is correct.

I am wondering where the equivalent code should live in the OpenZeppelin implementation, as the constructor there only seems to set the underlying token (not vToken):

constructor(IERC20 asset_) {
        (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
        _underlyingDecimals = success ? assetDecimals : 18;
        _asset = asset_;
    }

Is it the case that in the solmate implementation this initialization happens directly in the ERC4626 contract whereas with the OpenZeppelin ERC4626, the expectation is that this would happen in the higher-level vault contract that inherits from ERC4626?

Thanks again.

OZ doesn't dictate where you should initialize the token.

In the example that you gave here, the vault and the token are implemented in a single contract.

But they can also be implemented in two separate contracts, where the vault interacts with the token via external calls, while the token - being a standard ERC20 - remains completely oblivious to the vault.

In this paradigm, the token is deployed first and the vault is deployed second, receiving the address of the token either upon deployment (in the constructor) or after deployment (in an admin function).

IMO, this paradigm is more robust and more correct, but you can continue reading and decide for yourself...

Some of the upsides in implementing them as two separate contracts:

  • You can implement only the vault as upgradable, while keeping the token as non-upgradable (which makes it more financially trustworthy in general)
  • The token contract looks a lot more like what a token contract should look like, exposing only token-related functions, such as transfer, approve, etc
  • You can implement more logic (functionality) in each one of them before exceeding the maximum byte-code size limit (24KB) per contract

Some of the downsides in implementing them as two separate contracts:

  • More expensive transactions, due to external calls between the two contracts
  • Higher security risk (requiring safety measures such as reentrancy-protection, etc)

Ok, got it.

So I could either instantiate the vault / share token in my Vault.sol contract that inherits ERC4626 or pass in an pre-existing, pre-deployed vault token address via constructor or admin function. Makes sense.

I do kind of like the vault and vToken being separate contracts however I could also see the argument for combining them since ERC4626 vault can also be looked at as an 'ERC20 token with vault capabilities'. Tough call. I'll have to consider some of the pros and cons that you outlined.

Thanks for the info - much appreciated.

1 Like