ERC4626 Proxy Contract: Changing underlying token

Once I deploy an upgradeable ERC4626 vault token contract (via Proxy), would I be able to upgrade the contract to with a different underlying asset token?

Yes, by implementing (before deploying your contract) something like this:

import { ERC4626Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";

contract MyContract is ERC4626Upgradeable {
    constructor(IERC20 asset_) {
        initialize(asset_);
    }

    function initialize(IERC20 asset_) public initializer {
        __MyContract_init(asset_);
    }

    function __MyContract_init(IERC20 asset_) internal onlyInitializing {
        __ERC4626_init_unchained(asset_);
        __MyContract_init_unchained(asset_);
    }

    function __MyContract_init_unchained(IERC20 asset_) internal onlyInitializing {
        // Whatever else you need to initialize here...
    }
}

Thanks for your response! I'm not sure if the constructor is needed here. Plus, is it possible to do something like this? I wonder if we can call __ERC4626_init more than once or anywhere else outside of the (re)initializer functions.

contract MyContract is ERC4626Upgradeable {
    function initialize(IERC20 asset_) public initializer {
        __ERC4626_init(asset_);
    }

    /**
     * After the first deployment, calling upgrades.upgradeProxy() would
     * in turn call this and effectively update the asset_ state variable inside:
     *
     * /// @custom:storage-location erc7201:openzeppelin.storage.ERC4626
     * struct ERC4626Storage {
     *      IERC20 _asset;
     *      uint8 _underlyingDecimals;
     * }
     *
     * */
    function initializeV2(IERC20 newAsset_) public reinitializer(2) {
        __ERC4626_init(newAsset_);
    }
}

Yes, the constructor in my example is optional (it would make your very first deployment easier, since you wouldn't need to call function initialize immediately afterwards).

I cannot see why you'd need something like this, when you can call function initialize.

The problem is that initialize() contains the initializer modifier that defines a protected initializer function that can be invoked at most once according to the Initializable.sol. I referred to this https://docs.openzeppelin.com/contracts/5.x/api/proxy#Initializable

Ah yeah, I was thinking in OZ-v4.9 terms.

OZ-v5.0 has probably introduced the reinitializer modifier in order to support that.

Yes, so hopefully someone can confirm if I could call this __ERC4626_init (IERC20 asset_) more than once!

Well, you can just take a look at this function and see for yourself (TLDR, It looks legit).