From 712750f8e6f21772ae82f15db7dc0af34027ae4f Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:03:18 +0530 Subject: [PATCH 1/2] feat: beacon proxify tokenization spoke --- snapshots/TokenizationSpoke.Operations.json | 30 +-- src/config-engine/README.md | 22 +-- .../interfaces/IAaveV4ConfigEngine.sol | 2 + src/config-engine/libraries/HubEngine.sol | 1 + .../libraries/TokenizationSpokeDeployer.sol | 93 +++------ src/dependencies/openzeppelin/BeaconProxy.sol | 57 ++++++ .../openzeppelin/UpgradeableBeacon.sol | 70 +++++++ src/deployments/README.md | 4 +- .../batches/AaveV4TokenizationSpokeBatch.sol | 26 ++- .../AaveV4TokenizationSpokeBeaconBatch.sol | 36 ++++ src/deployments/libraries/BatchReports.sol | 11 +- .../orchestration/AaveV4DeployBase.sol | 24 ++- ...TokenizationSpokeBeaconDeployProcedure.sol | 41 ++++ ...AaveV4TokenizationSpokeDeployProcedure.sol | 47 ++--- .../interfaces/ITokenizationSpokeInstance.sol | 11 +- .../utils/libraries/Create2Utils.sol | 18 ++ src/spoke/TokenizationSpoke.sol | 87 +++++---- src/spoke/TokenizationSpokeStorage.sol | 25 +++ .../instances/TokenizationSpokeInstance.sol | 14 +- tests/config-engine/BaseConfigEngine.t.sol | 16 +- tests/config-engine/HubEngine.t.sol | 26 ++- .../TokenizationSpoke.Config.t.sol | 39 +++- .../TokenizationSpoke.Upgradeable.t.sol | 179 ++++++++++-------- .../AaveV4TokenizationSpokeBatch.t.sol | 56 ++++-- tests/deployments/batches/BatchBase.t.sol | 1 + .../orchestration/AaveV4TestOrchestration.sol | 15 +- ...veV4TokenizationSpokeDeployProcedure.t.sol | 71 ++++--- .../TokenizationSpoke.Operations.gas.t.sol | 2 +- .../mocks/MockTokenizationSpokeInstance.sol | 15 +- ...okenizationSpokeDeployProcedureWrapper.sol | 16 +- .../tokenization-spoke/SetupHelpers.sol | 42 ++-- tests/utils/ProxyHelper.sol | 7 + 32 files changed, 724 insertions(+), 380 deletions(-) create mode 100644 src/dependencies/openzeppelin/BeaconProxy.sol create mode 100644 src/dependencies/openzeppelin/UpgradeableBeacon.sol create mode 100644 src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol create mode 100644 src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeBeaconDeployProcedure.sol create mode 100644 src/spoke/TokenizationSpokeStorage.sol diff --git a/snapshots/TokenizationSpoke.Operations.json b/snapshots/TokenizationSpoke.Operations.json index 9568365f8..8427db0d0 100644 --- a/snapshots/TokenizationSpoke.Operations.json +++ b/snapshots/TokenizationSpoke.Operations.json @@ -1,17 +1,17 @@ { - "deposit": "118614", - "depositWithSig": "129518", - "mint": "118251", - "mintWithSig": "129130", - "permit": "62766", - "redeem: on behalf, full": "95212", - "redeem: on behalf, partial": "119015", - "redeem: self, full": "94282", - "redeem: self, partial": "113482", - "redeemWithSig": "128864", - "withdraw: on behalf, full": "95646", - "withdraw: on behalf, partial": "119557", - "withdraw: self, full": "94824", - "withdraw: self, partial": "114024", - "withdrawWithSig": "129405" + "deposit": "128151", + "depositWithSig": "139020", + "mint": "127788", + "mintWithSig": "138620", + "permit": "65752", + "redeem: on behalf, full": "102440", + "redeem: on behalf, partial": "126440", + "redeem: self, full": "101707", + "redeem: self, partial": "120907", + "redeemWithSig": "136245", + "withdraw: on behalf, full": "102993", + "withdraw: on behalf, partial": "126993", + "withdraw: self, full": "102260", + "withdraw: self, partial": "121460", + "withdrawWithSig": "136821" } \ No newline at end of file diff --git a/src/config-engine/README.md b/src/config-engine/README.md index bc3b8a985..23231352d 100644 --- a/src/config-engine/README.md +++ b/src/config-engine/README.md @@ -22,17 +22,17 @@ The four groups, and the virtual functions in each, are listed below. #### Hub actions (`_executeHubActions`) -| Function | Struct | Purpose | -| ----------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- | -| `hubAssetListings()` | `AssetListing` | List a new asset on a Hub. Optionally deploys a TokenizationSpoke if `symbol` `and name` are defined. | -| `hubAssetConfigUpdates()` | `AssetConfigUpdate` | Update fee config, IR strategy/data, reinvestment controller | -| `hubSpokeToAssetsAdditions()` | `SpokeToAssetsAddition` | Register a Spoke for multiple assets | -| `hubSpokeConfigUpdates()` | `SpokeConfigUpdate` | Update Spoke caps, risk premium threshold, active/halted | -| `hubAssetHalts()` | `AssetHalt` | Halt an asset | -| `hubAssetDeactivations()` | `AssetDeactivation` | Deactivate an asset | -| `hubAssetCapsResets()` | `AssetCapsReset` | Reset asset caps | -| `hubSpokeDeactivations()` | `SpokeDeactivation` | Deactivate a Spoke | -| `hubSpokeCapsResets()` | `SpokeCapsReset` | Reset Spoke caps | +| Function | Struct | Purpose | +| ----------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `hubAssetListings()` | `AssetListing` | List a new asset on a Hub. Optionally deploys a TokenizationSpoke beacon proxy (pointing to `tokenization.beacon`) if `symbol` `and name` are defined. | +| `hubAssetConfigUpdates()` | `AssetConfigUpdate` | Update fee config, IR strategy/data, reinvestment controller | +| `hubSpokeToAssetsAdditions()` | `SpokeToAssetsAddition` | Register a Spoke for multiple assets | +| `hubSpokeConfigUpdates()` | `SpokeConfigUpdate` | Update Spoke caps, risk premium threshold, active/halted | +| `hubAssetHalts()` | `AssetHalt` | Halt an asset | +| `hubAssetDeactivations()` | `AssetDeactivation` | Deactivate an asset | +| `hubAssetCapsResets()` | `AssetCapsReset` | Reset asset caps | +| `hubSpokeDeactivations()` | `SpokeDeactivation` | Deactivate a Spoke | +| `hubSpokeCapsResets()` | `SpokeCapsReset` | Reset Spoke caps | #### Spoke actions (`_executeSpokeActions`) diff --git a/src/config-engine/interfaces/IAaveV4ConfigEngine.sol b/src/config-engine/interfaces/IAaveV4ConfigEngine.sol index 2dacaac8e..042f900f9 100644 --- a/src/config-engine/interfaces/IAaveV4ConfigEngine.sol +++ b/src/config-engine/interfaces/IAaveV4ConfigEngine.sol @@ -15,10 +15,12 @@ import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateS /// the universal KEEP_CURRENT sentinel. Boolean fields use uint256 (0=false, 1=true, KEEP_CURRENT=skip). interface IAaveV4ConfigEngine { /// @notice Parameters for tokenization of an asset on a Hub when listing the asset. + /// @dev beacon The shared TokenizationSpoke beacon the deployed proxy points to. /// @dev addCap The add cap for the TokenizationSpoke (0 means no tokenization). /// @dev name The name for the TokenizationSpoke. /// @dev symbol The symbol for the TokenizationSpoke. struct TokenizationSpokeConfig { + address beacon; uint256 addCap; string name; string symbol; diff --git a/src/config-engine/libraries/HubEngine.sol b/src/config-engine/libraries/HubEngine.sol index 20b3d68c7..9d0a777c0 100644 --- a/src/config-engine/libraries/HubEngine.sol +++ b/src/config-engine/libraries/HubEngine.sol @@ -227,6 +227,7 @@ library HubEngine { } address proxy = TokenizationSpokeDeployer.deploy( + listing.tokenization.beacon, listing.hub, listing.underlying, listing.tokenization.name, diff --git a/src/config-engine/libraries/TokenizationSpokeDeployer.sol b/src/config-engine/libraries/TokenizationSpokeDeployer.sol index 193b5da57..5f807725e 100644 --- a/src/config-engine/libraries/TokenizationSpokeDeployer.sol +++ b/src/config-engine/libraries/TokenizationSpokeDeployer.sol @@ -1,114 +1,75 @@ // SPDX-License-Identifier: LicenseRef-BUSL pragma solidity ^0.8.0; -import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {BeaconProxy} from 'src/dependencies/openzeppelin/BeaconProxy.sol'; import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; -import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; +import {ITokenizationSpokeInstance} from 'src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol'; /// @title TokenizationSpokeDeployer /// @author Aave Labs -/// @notice Library for deterministic CREATE2 deployment and address pre-computation of TokenizationSpoke proxies -/// using the Safe Singleton Factory. +/// @notice Library for deterministic CREATE2 deployment and address pre-computation of TokenizationSpoke +/// beacon proxies using the Safe Singleton Factory. library TokenizationSpokeDeployer { - /// @notice Deploys a TokenizationSpokeInstance implementation and TransparentUpgradeableProxy via CREATE2 - /// through the Safe Singleton Factory. - /// @dev The proxy admin owner is set to `msg.sender`. + /// @notice Deploys a TokenizationSpoke BeaconProxy via CREATE2 through the Safe Singleton Factory. + /// @dev The proxy points to the shared `beacon`, whose owner controls the implementation upgrades. + /// @param beacon The address of the shared TokenizationSpoke beacon. /// @param hub The address of the Hub. /// @param underlying The address of the underlying asset. /// @param name The ERC20 name for the TokenizationSpoke share token. /// @param symbol The ERC20 symbol for the TokenizationSpoke share token. /// @return proxy The address of the deployed proxy. function deploy( + address beacon, address hub, address underlying, string calldata name, string calldata symbol ) external returns (address proxy) { - bytes32 implSalt = _computeImplementationSalt(hub, underlying, name, symbol); - bytes memory implCreationCode = abi.encodePacked( - type(TokenizationSpokeInstance).creationCode, - abi.encode(hub, underlying) + bytes32 proxySalt = _computeProxySalt(beacon, hub, underlying, name, symbol); + bytes memory initData = abi.encodeCall( + ITokenizationSpokeInstance.initialize, + (hub, underlying, name, symbol) ); - address impl = Create2Utils.create2Deploy(implSalt, implCreationCode); - - bytes32 proxySalt = _computeProxySalt(hub, underlying, name, symbol); - bytes memory initData = abi.encodeCall(TokenizationSpokeInstance.initialize, (name, symbol)); bytes memory proxyCreationCode = abi.encodePacked( - type(TransparentUpgradeableProxy).creationCode, - abi.encode(impl, msg.sender, initData) + type(BeaconProxy).creationCode, + abi.encode(beacon, initData) ); proxy = Create2Utils.create2Deploy(proxySalt, proxyCreationCode); } - /// @notice Pre-computes the CREATE2 address of the TokenizationSpokeInstance implementation. + /// @notice Pre-computes the CREATE2 address of the TokenizationSpoke BeaconProxy. + /// @param beacon The address of the shared TokenizationSpoke beacon. /// @param hub The address of the Hub. /// @param underlying The address of the underlying asset. /// @param name The ERC20 name for the TokenizationSpoke share token. /// @param symbol The ERC20 symbol for the TokenizationSpoke share token. - /// @return The predicted implementation address. - function computeImplementationAddress( - address hub, - address underlying, - string memory name, - string memory symbol - ) external pure returns (address) { - return _computeImplementationAddress(hub, underlying, name, symbol); - } - - /// @notice Pre-computes the CREATE2 address of the TransparentUpgradeableProxy. - /// @param hub The address of the Hub. - /// @param underlying The address of the underlying asset. - /// @param name The ERC20 name for the TokenizationSpoke share token. - /// @param symbol The ERC20 symbol for the TokenizationSpoke share token. - /// @param proxyAdminOwner The initial owner of the ProxyAdmin (msg.sender in `deploy`). /// @return The predicted proxy address. function computeProxyAddress( + address beacon, address hub, address underlying, string memory name, - string memory symbol, - address proxyAdminOwner + string memory symbol ) external pure returns (address) { - address impl = _computeImplementationAddress(hub, underlying, name, symbol); - - bytes32 proxySalt = _computeProxySalt(hub, underlying, name, symbol); - bytes memory initData = abi.encodeCall(TokenizationSpokeInstance.initialize, (name, symbol)); - bytes memory creationCode = abi.encodePacked( - type(TransparentUpgradeableProxy).creationCode, - abi.encode(impl, proxyAdminOwner, initData) + bytes32 proxySalt = _computeProxySalt(beacon, hub, underlying, name, symbol); + bytes memory initData = abi.encodeCall( + ITokenizationSpokeInstance.initialize, + (hub, underlying, name, symbol) ); - return Create2Utils.computeCreate2Address(proxySalt, creationCode); - } - - function _computeImplementationAddress( - address hub, - address underlying, - string memory name, - string memory symbol - ) internal pure returns (address) { - bytes32 implSalt = _computeImplementationSalt(hub, underlying, name, symbol); bytes memory creationCode = abi.encodePacked( - type(TokenizationSpokeInstance).creationCode, - abi.encode(hub, underlying) + type(BeaconProxy).creationCode, + abi.encode(beacon, initData) ); - return Create2Utils.computeCreate2Address(implSalt, creationCode); - } - - function _computeImplementationSalt( - address hub, - address underlying, - string memory name, - string memory symbol - ) internal pure returns (bytes32) { - return keccak256(abi.encode(hub, underlying, name, symbol, 'impl')); + return Create2Utils.computeCreate2Address(proxySalt, creationCode); } function _computeProxySalt( + address beacon, address hub, address underlying, string memory name, string memory symbol ) internal pure returns (bytes32) { - return keccak256(abi.encode(hub, underlying, name, symbol, 'proxy')); + return keccak256(abi.encode(beacon, hub, underlying, name, symbol, 'proxy')); } } diff --git a/src/dependencies/openzeppelin/BeaconProxy.sol b/src/dependencies/openzeppelin/BeaconProxy.sol new file mode 100644 index 000000000..d5d63f77f --- /dev/null +++ b/src/dependencies/openzeppelin/BeaconProxy.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/beacon/BeaconProxy.sol) + +pragma solidity ^0.8.22; + +import {IBeacon} from "./IBeacon.sol"; +import {Proxy} from "./Proxy.sol"; +import {ERC1967Utils} from "./ERC1967Utils.sol"; + +/** + * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. + * + * The beacon address can only be set once during construction, and cannot be changed afterwards. It is stored in an + * immutable variable to avoid unnecessary storage reads, and also in the beacon storage slot specified by + * https://eips.ethereum.org/EIPS/eip-1967[ERC-1967] so that it can be accessed externally. + * + * CAUTION: Since the beacon address can never be changed, you must ensure that you either control the beacon, or trust + * the beacon to not upgrade the implementation maliciously. + * + * IMPORTANT: Do not use the implementation logic to modify the beacon storage slot. Doing so would leave the proxy in + * an inconsistent state where the beacon storage slot does not match the beacon address. + */ +contract BeaconProxy is Proxy { + // An immutable address for the beacon to avoid unnecessary SLOADs before each delegate call. + address private immutable _beacon; + + /** + * @dev Initializes the proxy with `beacon`. + * + * If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This + * will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity + * constructor. + * + * Requirements: + * + * - `beacon` must be a contract with the interface {IBeacon}. + * - If `data` is empty, `msg.value` must be zero. + */ + constructor(address beacon, bytes memory data) payable { + ERC1967Utils.upgradeBeaconToAndCall(beacon, data); + _beacon = beacon; + } + + /** + * @dev Returns the current implementation address of the associated beacon. + */ + function _implementation() internal view virtual override returns (address) { + return IBeacon(_getBeacon()).implementation(); + } + + /** + * @dev Returns the beacon. + */ + function _getBeacon() internal view virtual returns (address) { + return _beacon; + } +} diff --git a/src/dependencies/openzeppelin/UpgradeableBeacon.sol b/src/dependencies/openzeppelin/UpgradeableBeacon.sol new file mode 100644 index 000000000..bf8a603b4 --- /dev/null +++ b/src/dependencies/openzeppelin/UpgradeableBeacon.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/UpgradeableBeacon.sol) + +pragma solidity ^0.8.20; + +import {IBeacon} from "./IBeacon.sol"; +import {Ownable} from "./Ownable.sol"; + +/** + * @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their + * implementation contract, which is where they will delegate all function calls. + * + * An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon. + */ +contract UpgradeableBeacon is IBeacon, Ownable { + address private _implementation; + + /** + * @dev The `implementation` of the beacon is invalid. + */ + error BeaconInvalidImplementation(address implementation); + + /** + * @dev Emitted when the implementation returned by the beacon is changed. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Sets the address of the initial implementation, and the initial owner who can upgrade the beacon. + */ + constructor(address implementation_, address initialOwner) Ownable(initialOwner) { + _setImplementation(implementation_); + } + + /** + * @dev Returns the current implementation address. + */ + function implementation() public view virtual returns (address) { + return _implementation; + } + + /** + * @dev Upgrades the beacon to a new implementation. + * + * Emits an {Upgraded} event. + * + * Requirements: + * + * - msg.sender must be the owner of the contract. + * - `newImplementation` must be a contract. + */ + function upgradeTo(address newImplementation) public virtual onlyOwner { + _setImplementation(newImplementation); + } + + /** + * @dev Sets the implementation contract address for this beacon + * + * Requirements: + * + * - `newImplementation` must be a contract. + */ + function _setImplementation(address newImplementation) private { + if (newImplementation.code.length == 0) { + revert BeaconInvalidImplementation(newImplementation); + } + _implementation = newImplementation; + emit Upgraded(newImplementation); + } +} diff --git a/src/deployments/README.md b/src/deployments/README.md index 17cbc2592..cf05b3455 100644 --- a/src/deployments/README.md +++ b/src/deployments/README.md @@ -36,7 +36,9 @@ This runs `AaveV4DeployOrchestration.deployAaveV4()`, which deploys batches in o ### TokenizationSpoke -`TokenizationSpoke` is **not** deployed by the orchestration, because it requires an asset to already be listed on a Hub and Spoke. Each `TokenizationSpoke` instance should be deployed separately after asset listing, one per asset. +`TokenizationSpoke` is **not** deployed by the orchestration, because it requires an asset to already be listed on a Hub and Spoke. + +`TokenizationSpoke` uses a **beacon proxy** pattern: a single shared implementation and `UpgradeableBeacon` are deployed once (via `AaveV4TokenizationSpokeBeaconBatch`), and every per-asset `TokenizationSpoke` is a `BeaconProxy` pointing to that beacon (via `AaveV4TokenizationSpokeBatch`). Because the implementation is generic, the Hub and tokenized asset details are passed to `initialize` rather than baked into the implementation bytecode, so all assets reuse the same implementation. The beacon owner controls upgrades for every `TokenizationSpoke` at once. Each `TokenizationSpoke` proxy should be deployed separately after asset listing, one per asset. ### LiquidationLogic Pre-deployment diff --git a/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol b/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol index ddd31ea79..0500deb44 100644 --- a/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol +++ b/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol @@ -7,39 +7,35 @@ import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; /// @title AaveV4TokenizationSpokeBatch /// @author Aave Labs -/// @notice Deploys a TokenizationSpoke instance (proxy + implementation), producing a batch report. +/// @notice Deploys a TokenizationSpoke instance (beacon proxy) for a given asset, producing a batch report. contract AaveV4TokenizationSpokeBatch is AaveV4TokenizationSpokeDeployProcedure { BatchReports.TokenizationSpokeBatchReport internal _report; /// @dev Constructor. + /// @param beacon_ The address of the shared TokenizationSpoke beacon. /// @param hub_ The address of the Hub the TokenizationSpoke connects to. /// @param underlying_ The address of the underlying asset to tokenize. - /// @param proxyAdminOwner_ The owner of the proxy admin. /// @param shareName_ The name of the share token. /// @param shareSymbol_ The symbol of the share token. /// @param salt_ The CREATE2 salt for deterministic deployment. constructor( + address beacon_, address hub_, address underlying_, - address proxyAdminOwner_, string memory shareName_, string memory shareSymbol_, bytes32 salt_ ) { - ( - address tokenizationSpokeProxy, - address tokenizationSpokeImplementation - ) = _deployUpgradeableTokenizationSpokeInstance({ - hub: hub_, - underlying: underlying_, - proxyAdminOwner: proxyAdminOwner_, - shareName: shareName_, - shareSymbol: shareSymbol_, - salt: salt_ - }); + address tokenizationSpokeProxy = _deployTokenizationSpokeInstance({ + beacon: beacon_, + hub: hub_, + underlying: underlying_, + shareName: shareName_, + shareSymbol: shareSymbol_, + salt: salt_ + }); _report = BatchReports.TokenizationSpokeBatchReport({ - tokenizationSpokeImplementation: tokenizationSpokeImplementation, tokenizationSpokeProxy: tokenizationSpokeProxy }); } diff --git a/src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol b/src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol new file mode 100644 index 000000000..e459a5e13 --- /dev/null +++ b/src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AaveV4TokenizationSpokeBeaconDeployProcedure} from 'src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeBeaconDeployProcedure.sol'; + +/// @title AaveV4TokenizationSpokeBeaconBatch +/// @author Aave Labs +/// @notice Deploys the shared TokenizationSpoke implementation and its UpgradeableBeacon, producing a batch report. +contract AaveV4TokenizationSpokeBeaconBatch is AaveV4TokenizationSpokeBeaconDeployProcedure { + BatchReports.TokenizationSpokeBeaconBatchReport internal _report; + + /// @dev Constructor. + /// @param beaconOwner_ The owner of the beacon, able to upgrade the shared implementation. + /// @param salt_ The CREATE2 salt for deterministic deployment. + constructor(address beaconOwner_, bytes32 salt_) { + ( + address tokenizationSpokeBeacon, + address tokenizationSpokeImplementation + ) = _deployTokenizationSpokeBeacon({beaconOwner: beaconOwner_, salt: salt_}); + + _report = BatchReports.TokenizationSpokeBeaconBatchReport({ + tokenizationSpokeImplementation: tokenizationSpokeImplementation, + tokenizationSpokeBeacon: tokenizationSpokeBeacon + }); + } + + /// @notice Returns the batch deployment report. + function getReport() + external + view + returns (BatchReports.TokenizationSpokeBeaconBatchReport memory) + { + return _report; + } +} diff --git a/src/deployments/libraries/BatchReports.sol b/src/deployments/libraries/BatchReports.sol index b05070568..48bc0ca37 100644 --- a/src/deployments/libraries/BatchReports.sol +++ b/src/deployments/libraries/BatchReports.sol @@ -56,10 +56,15 @@ library BatchReports { address configPositionManager; } - /// @dev tokenizationSpokeImplementation The deployed TokenizationSpoke implementation contract address. - /// @dev tokenizationSpokeProxy The deployed TokenizationSpoke proxy contract address. + /// @dev tokenizationSpokeImplementation The deployed shared TokenizationSpoke implementation contract address. + /// @dev tokenizationSpokeBeacon The deployed UpgradeableBeacon contract address. + struct TokenizationSpokeBeaconBatchReport { + address tokenizationSpokeImplementation; + address tokenizationSpokeBeacon; + } + + /// @dev tokenizationSpokeProxy The deployed TokenizationSpoke beacon proxy contract address. struct TokenizationSpokeBatchReport { address tokenizationSpokeProxy; - address tokenizationSpokeImplementation; } } diff --git a/src/deployments/orchestration/AaveV4DeployBase.sol b/src/deployments/orchestration/AaveV4DeployBase.sol index 5fef5ee8e..39a80c46a 100644 --- a/src/deployments/orchestration/AaveV4DeployBase.sol +++ b/src/deployments/orchestration/AaveV4DeployBase.sol @@ -10,6 +10,7 @@ import {AaveV4HubInstanceBatch} from 'src/deployments/batches/AaveV4HubInstanceB import {AaveV4PositionManagerBatch} from 'src/deployments/batches/AaveV4PositionManagerBatch.sol'; import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInstanceBatch.sol'; import {AaveV4TokenizationSpokeBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBatch.sol'; +import {AaveV4TokenizationSpokeBeaconBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol'; import {AaveV4TreasurySpokeBatch} from 'src/deployments/batches/AaveV4TreasurySpokeBatch.sol'; /// @title AaveV4DeployBase Library @@ -148,26 +149,41 @@ library AaveV4DeployBase { return gatewayBatch.getReport(); } - /// @notice Deploys the Tokenization Spoke batch containing the TokenizationSpoke proxy and implementation. + /// @notice Deploys the Tokenization Spoke beacon batch containing the shared implementation and beacon. + /// @param beaconOwner The owner of the beacon, able to upgrade the shared implementation. + /// @param salt The CREATE2 salt for deterministic deployment. + /// @return The Tokenization Spoke beacon batch report. + function deployTokenizationSpokeBeaconBatch( + address beaconOwner, + bytes32 salt + ) internal returns (BatchReports.TokenizationSpokeBeaconBatchReport memory) { + AaveV4TokenizationSpokeBeaconBatch tokenizationSpokeBeaconBatch = new AaveV4TokenizationSpokeBeaconBatch({ + beaconOwner_: beaconOwner, + salt_: salt + }); + return tokenizationSpokeBeaconBatch.getReport(); + } + + /// @notice Deploys the Tokenization Spoke batch containing the TokenizationSpoke beacon proxy. + /// @param beacon The address of the shared TokenizationSpoke beacon. /// @param hub The address of the Hub the tokenization spoke connects to. /// @param underlying The address of the underlying asset to tokenize. - /// @param proxyAdminOwner The owner of the proxy admin. /// @param shareName The name of the share token. /// @param shareSymbol The symbol of the share token. /// @param salt The CREATE2 salt for deterministic deployment. /// @return The Tokenization Spoke batch report. function deployTokenizationSpokeBatch( + address beacon, address hub, address underlying, - address proxyAdminOwner, string memory shareName, string memory shareSymbol, bytes32 salt ) internal returns (BatchReports.TokenizationSpokeBatchReport memory) { AaveV4TokenizationSpokeBatch tokenizationSpokeBatch = new AaveV4TokenizationSpokeBatch({ + beacon_: beacon, hub_: hub, underlying_: underlying, - proxyAdminOwner_: proxyAdminOwner, shareName_: shareName, shareSymbol_: shareSymbol, salt_: salt diff --git a/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeBeaconDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeBeaconDeployProcedure.sol new file mode 100644 index 000000000..9be7700d6 --- /dev/null +++ b/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeBeaconDeployProcedure.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; +import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; +import {UpgradeableBeacon} from 'src/dependencies/openzeppelin/UpgradeableBeacon.sol'; +import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; + +/// @title AaveV4TokenizationSpokeBeaconDeployProcedure +/// @author Aave Labs +/// @notice Deploys the shared TokenizationSpoke implementation and its UpgradeableBeacon. +/// @dev All TokenizationSpoke beacon proxies share this single implementation and beacon, so the +/// beacon owner controls upgrades for every TokenizationSpoke that points to it. +contract AaveV4TokenizationSpokeBeaconDeployProcedure is AaveV4DeployProcedureBase { + /// @notice Deploys the shared TokenizationSpoke implementation and beacon via CREATE2. + /// @param beaconOwner The owner of the beacon, able to upgrade the shared implementation. + /// @param salt The CREATE2 salt for deterministic deployment. + /// @return tokenizationSpokeBeacon The address of the deployed beacon. + /// @return tokenizationSpokeImplementation The address of the deployed shared implementation contract. + function _deployTokenizationSpokeBeacon( + address beaconOwner, + bytes32 salt + ) internal returns (address tokenizationSpokeBeacon, address tokenizationSpokeImplementation) { + require(beaconOwner != address(0), 'invalid beacon owner'); + + tokenizationSpokeImplementation = Create2Utils.create2Deploy({ + salt: salt, + bytecode: type(TokenizationSpokeInstance).creationCode + }); + + tokenizationSpokeBeacon = Create2Utils.create2Deploy({ + salt: salt, + bytecode: abi.encodePacked( + type(UpgradeableBeacon).creationCode, + abi.encode(tokenizationSpokeImplementation, beaconOwner) + ) + }); + + return (tokenizationSpokeBeacon, tokenizationSpokeImplementation); + } +} diff --git a/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol b/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol index 531ccbc89..3ab46063f 100644 --- a/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol +++ b/src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol @@ -4,45 +4,40 @@ pragma solidity ^0.8.0; import {AaveV4DeployProcedureBase} from 'src/deployments/procedures/AaveV4DeployProcedureBase.sol'; import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; import {ITokenizationSpokeInstance} from 'src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol'; -import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; /// @title AaveV4TokenizationSpokeDeployProcedure /// @author Aave Labs -/// @notice Deploys an upgradeable TokenizationSpoke instance behind a transparent proxy. +/// @notice Deploys a TokenizationSpoke instance behind a beacon proxy. contract AaveV4TokenizationSpokeDeployProcedure is AaveV4DeployProcedureBase { - /// @notice Deploys a TokenizationSpoke implementation via CREATE2 and sets up a transparent proxy. + /// @notice Deploys a TokenizationSpoke beacon proxy via CREATE2 and initializes it. + /// @param beacon The address of the shared TokenizationSpoke beacon. /// @param hub The address of the Hub that the tokenization spoke connects to. /// @param underlying The address of the underlying asset to tokenize. - /// @param proxyAdminOwner The owner of the proxy admin contract. /// @param shareName The name of the share token. /// @param shareSymbol The symbol of the share token. /// @param salt The CREATE2 salt for deterministic deployment. - /// @return tokenizationSpokeProxy The address of the deployed transparent proxy. - /// @return tokenizationSpokeImplementation The address of the deployed TokenizationSpoke implementation contract. - function _deployUpgradeableTokenizationSpokeInstance( + /// @return tokenizationSpokeProxy The address of the deployed beacon proxy. + function _deployTokenizationSpokeInstance( + address beacon, address hub, address underlying, - address proxyAdminOwner, string memory shareName, string memory shareSymbol, bytes32 salt - ) internal returns (address tokenizationSpokeProxy, address tokenizationSpokeImplementation) { + ) internal returns (address tokenizationSpokeProxy) { + require(beacon != address(0), 'invalid beacon'); require(hub != address(0), 'invalid hub'); - require(proxyAdminOwner != address(0), 'invalid proxy admin owner'); require(bytes(shareName).length > 0, 'invalid share name'); require(bytes(shareSymbol).length > 0, 'invalid share symbol'); - tokenizationSpokeImplementation = Create2Utils.create2Deploy({ + tokenizationSpokeProxy = Create2Utils.beaconProxify({ salt: salt, - bytecode: _getTokenizationSpokeInstanceInitCode(hub, underlying) - }); - - tokenizationSpokeProxy = Create2Utils.proxify({ - salt: salt, - logic: tokenizationSpokeImplementation, - initialOwner: proxyAdminOwner, - data: abi.encodeCall(ITokenizationSpokeInstance.initialize, (shareName, shareSymbol)) + beacon: beacon, + data: abi.encodeCall( + ITokenizationSpokeInstance.initialize, + (hub, underlying, shareName, shareSymbol) + ) }); require( @@ -54,18 +49,6 @@ contract AaveV4TokenizationSpokeDeployProcedure is AaveV4DeployProcedureBase { 'tokenization spoke underlying mismatch' ); - return (tokenizationSpokeProxy, tokenizationSpokeImplementation); - } - - /// @notice Returns the creation bytecode for a TokenizationSpokeInstance with constructor arguments appended. - /// @param hub The address of the Hub contract. - /// @param underlying The address of the underlying asset. - /// @return The ABI-encoded creation bytecode. - function _getTokenizationSpokeInstanceInitCode( - address hub, - address underlying - ) internal pure returns (bytes memory) { - return - abi.encodePacked(type(TokenizationSpokeInstance).creationCode, abi.encode(hub, underlying)); + return tokenizationSpokeProxy; } } diff --git a/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol b/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol index f8be1cbf8..c8f81bcac 100644 --- a/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol +++ b/src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol @@ -7,10 +7,17 @@ import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; /// @author Aave Labs /// @notice TokenizationSpoke instance interface exposing the initializer and revision. interface ITokenizationSpokeInstance is ITokenizationSpoke { - /// @notice Initializes the TokenizationSpoke instance with the given share token metadata. + /// @notice Initializes the TokenizationSpoke instance with the Hub, tokenized asset, and share token metadata. + /// @param hub The address of the associated Hub. + /// @param underlying The address of the underlying asset to be tokenized. /// @param shareName The name of the share token. /// @param shareSymbol The symbol of the share token. - function initialize(string memory shareName, string memory shareSymbol) external; + function initialize( + address hub, + address underlying, + string memory shareName, + string memory shareSymbol + ) external; /// @notice Returns the revision number of this TokenizationSpoke implementation. function SPOKE_REVISION() external view returns (uint64); diff --git a/src/deployments/utils/libraries/Create2Utils.sol b/src/deployments/utils/libraries/Create2Utils.sol index f380ffb8f..be5ba69f5 100644 --- a/src/deployments/utils/libraries/Create2Utils.sol +++ b/src/deployments/utils/libraries/Create2Utils.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.20; import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {BeaconProxy} from 'src/dependencies/openzeppelin/BeaconProxy.sol'; /// @title Create2Utils Library /// @author Aave Labs @@ -54,6 +55,23 @@ library Create2Utils { ); } + /// @notice Deploys a BeaconProxy via CREATE2. + /// @param salt The CREATE2 salt. + /// @param beacon The beacon contract address that determines the implementation. + /// @param data The initializer calldata. + /// @return The deployed proxy address. + function beaconProxify( + bytes32 salt, + address beacon, + bytes memory data + ) internal returns (address) { + return + create2Deploy( + salt, + abi.encodePacked(type(BeaconProxy).creationCode, abi.encode(beacon, data)) + ); + } + /// @notice Returns true if the address has deployed code. function isContractDeployed(address _addr) internal view returns (bool isContract) { return (_addr.code.length > 0); diff --git a/src/spoke/TokenizationSpoke.sol b/src/spoke/TokenizationSpoke.sol index 47f611d68..19afd600c 100644 --- a/src/spoke/TokenizationSpoke.sol +++ b/src/spoke/TokenizationSpoke.sol @@ -8,6 +8,7 @@ import {IERC4626, IERC20Metadata} from 'src/dependencies/openzeppelin/IERC4626.s import {IERC20Permit} from 'src/dependencies/openzeppelin/IERC20Permit.sol'; import {EIP712Hash} from 'src/spoke/libraries/EIP712Hash.sol'; import {MathUtils} from 'src/libraries/math/MathUtils.sol'; +import {TokenizationSpokeStorage} from 'src/spoke/TokenizationSpokeStorage.sol'; import {IntentConsumer} from 'src/utils/IntentConsumer.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; @@ -15,7 +16,12 @@ import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; /// @title TokenizationSpoke /// @author Aave Labs /// @notice ERC4626 compliant wrapper to tokenize one listed asset of the connected Hub. -abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, IntentConsumer { +abstract contract TokenizationSpoke is + ITokenizationSpoke, + TokenizationSpokeStorage, + ERC20Upgradeable, + IntentConsumer +{ using SafeERC20 for IERC20; using EIP712Hash for *; using MathUtils for uint256; @@ -32,35 +38,30 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int bytes32 public constant WITHDRAW_TYPEHASH = EIP712Hash.TOKENIZED_WITHDRAW_TYPEHASH; /// @inheritdoc ITokenizationSpoke bytes32 public constant REDEEM_TYPEHASH = EIP712Hash.TOKENIZED_REDEEM_TYPEHASH; - /// @inheritdoc ITokenizationSpoke - uint40 public immutable MAX_ALLOWED_SPOKE_CAP; - /// @dev Immutable references to the Hub and tokenized asset details. - IHub internal immutable HUB; - uint256 internal immutable ASSET_ID; - address internal immutable ASSET; - uint8 internal immutable DECIMALS; - uint256 internal immutable ASSET_UNITS; + /// @dev To be overridden by the inheriting TokenizationSpokeInstance contract. + function initialize( + address hub_, + address underlying_, + string memory shareName, + string memory shareSymbol + ) external virtual; - /// @dev Constructor. + /// @dev Sets the Hub and tokenized asset details and the vault share token's ERC20 name and + /// symbol. Must be called at first initialization. /// @param hub_ The address of the associated Hub contract. /// @param underlying_ The address of the underlying asset to be tokenized by this spoke. - constructor(address hub_, address underlying_) { - HUB = IHub(hub_); - ASSET_ID = HUB.getAssetId(underlying_); // reverts if invalid - (ASSET, DECIMALS) = HUB.getAssetUnderlyingAndDecimals(ASSET_ID); - ASSET_UNITS = MathUtils.uncheckedExp(10, DECIMALS); - MAX_ALLOWED_SPOKE_CAP = HUB.MAX_ALLOWED_SPOKE_CAP(); - } - - /// @dev To be overridden by the inheriting TokenizationSpokeInstance contract. - function initialize(string memory shareName, string memory shareSymbol) external virtual; - - /// @dev Sets the vault share token's ERC20 name and symbol. Must be called at first initialization. + /// @param shareName The ERC20 name of the share issued by this vault. + /// @param shareSymbol The ERC20 symbol of the share issued by this vault. function __TokenizationSpoke_init( + address hub_, + address underlying_, string memory shareName, string memory shareSymbol ) internal onlyInitializing { + _hub = IHub(hub_); + _assetId = _hub.getAssetId(underlying_); // reverts if invalid + (_asset, _decimals) = _hub.getAssetUnderlyingAndDecimals(_assetId); __ERC20_init(shareName, shareSymbol); } @@ -180,7 +181,7 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int bytes32 s ) external returns (uint256) { try - IERC20Permit(ASSET).permit({ + IERC20Permit(_asset).permit({ owner: msg.sender, spender: address(this), value: assets, @@ -235,22 +236,22 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int /// @inheritdoc IERC4626 function previewDeposit(uint256 assets) public view virtual returns (uint256) { - return HUB.previewAddByAssets(ASSET_ID, assets); + return _hub.previewAddByAssets(_assetId, assets); } /// @inheritdoc IERC4626 function previewMint(uint256 shares) public view virtual returns (uint256) { - return HUB.previewAddByShares(ASSET_ID, shares); + return _hub.previewAddByShares(_assetId, shares); } /// @inheritdoc IERC4626 function previewWithdraw(uint256 assets) public view virtual returns (uint256) { - return HUB.previewRemoveByAssets(ASSET_ID, assets); + return _hub.previewRemoveByAssets(_assetId, assets); } /// @inheritdoc IERC4626 function previewRedeem(uint256 shares) public view virtual returns (uint256) { - return HUB.previewRemoveByShares(ASSET_ID, shares); + return _hub.previewRemoveByShares(_assetId, shares); } /// @inheritdoc IERC4626 @@ -265,14 +266,15 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int /// @inheritdoc IERC4626 function maxDeposit(address) public view returns (uint256) { - IHub.SpokeConfig memory config = HUB.getSpokeConfig(ASSET_ID, address(this)); + IHub hubInstance = _hub; + IHub.SpokeConfig memory config = hubInstance.getSpokeConfig(_assetId, address(this)); if (!config.active || config.halted) { return 0; } - if (config.addCap == MAX_ALLOWED_SPOKE_CAP) { + if (config.addCap == hubInstance.MAX_ALLOWED_SPOKE_CAP()) { return type(uint256).max; } - uint256 allowed = config.addCap * ASSET_UNITS; + uint256 allowed = config.addCap * MathUtils.uncheckedExp(10, _decimals); uint256 balance = previewMint(totalSupply()); return allowed.zeroFloorSub(balance); } @@ -307,22 +309,27 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int /// @inheritdoc ITokenizationSpoke function hub() public view returns (address) { - return address(HUB); + return address(_hub); } /// @inheritdoc ITokenizationSpoke function assetId() public view returns (uint256) { - return ASSET_ID; + return _assetId; + } + + /// @inheritdoc ITokenizationSpoke + function MAX_ALLOWED_SPOKE_CAP() public view returns (uint40) { + return _hub.MAX_ALLOWED_SPOKE_CAP(); } /// @inheritdoc IERC4626 function asset() public view returns (address) { - return ASSET; + return _asset; } /// @inheritdoc IERC20Metadata function decimals() public view override(ERC20Upgradeable, IERC20Metadata) returns (uint8) { - return DECIMALS; + return _decimals; } /// @inheritdoc IERC20Permit @@ -416,14 +423,15 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int /// @dev Pulls the underlying asset from `from` and deposits it into the Hub. /// @dev Added shares in the Hub should match the minted shares in `_deposit`. function _pullAndDepositAssets(address from, uint256 amount) internal virtual { - IERC20(ASSET).safeTransferFrom(from, address(HUB), amount); - HUB.add(ASSET_ID, amount); + IHub hubInstance = _hub; + IERC20(_asset).safeTransferFrom(from, address(hubInstance), amount); + hubInstance.add(_assetId, amount); } /// @dev Removes the underlying asset from the Hub and pushes it to `to`. /// @dev Removed shares in the Hub should match the burned shares in `_withdraw`. function _removeAndPushAssets(address to, uint256 amount) internal virtual { - HUB.remove(ASSET_ID, amount, to); + _hub.remove(_assetId, amount, to); } /// @dev Hook that is called after any deposit or mint. @@ -433,11 +441,12 @@ abstract contract TokenizationSpoke is ITokenizationSpoke, ERC20Upgradeable, Int function _beforeWithdraw(uint256 assets, uint256 shares) internal virtual {} function _maxRemovableAssets() internal view returns (uint256) { - IHub.SpokeConfig memory config = HUB.getSpokeConfig(ASSET_ID, address(this)); + IHub hubInstance = _hub; + IHub.SpokeConfig memory config = hubInstance.getSpokeConfig(_assetId, address(this)); if (!config.active || config.halted) { return 0; } - return HUB.getAssetLiquidity(ASSET_ID); + return hubInstance.getAssetLiquidity(_assetId); } function _domainNameAndVersion() internal pure override returns (string memory, string memory) { diff --git a/src/spoke/TokenizationSpokeStorage.sol b/src/spoke/TokenizationSpokeStorage.sol new file mode 100644 index 000000000..d29038f13 --- /dev/null +++ b/src/spoke/TokenizationSpokeStorage.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity 0.8.28; + +import {IHub} from 'src/hub/interfaces/IHub.sol'; + +/// @title TokenizationSpokeStorage +/// @author Aave Labs +/// @notice Storage layout for the TokenizationSpoke contract. +/// @dev This contract defines all storage variables used by TokenizationSpoke. +abstract contract TokenizationSpokeStorage { + /// @dev The associated Hub contract. + IHub internal _hub; + + /// @dev The identifier of the tokenized asset on the Hub. + uint256 internal _assetId; // todo: change to 96 to save an sload, prob ok limitation + + /// @dev The address of the underlying asset to be tokenized. + address internal _asset; + + /// @dev The decimals of the share token, mirroring the underlying asset decimals. + uint8 internal _decimals; + + /// @dev Reserved storage space to allow for future layout updates. + uint256[50] private __gap; +} diff --git a/src/spoke/instances/TokenizationSpokeInstance.sol b/src/spoke/instances/TokenizationSpokeInstance.sol index 8e41ccd38..9a0d5587e 100644 --- a/src/spoke/instances/TokenizationSpokeInstance.sol +++ b/src/spoke/instances/TokenizationSpokeInstance.sol @@ -6,25 +6,29 @@ import {TokenizationSpoke} from 'src/spoke/TokenizationSpoke.sol'; /// @title TokenizationSpokeInstance /// @author Aave Labs /// @notice Implementation contract for the TokenizationSpoke. +/// @dev A single instance is shared by all TokenizationSpoke beacon proxies, so the Hub and +/// tokenized asset details are provided to the initializer rather than the constructor. contract TokenizationSpokeInstance is TokenizationSpoke { uint64 public constant SPOKE_REVISION = 1; /// @dev Constructor. - /// @param hub_ The address of the associated Hub. - /// @param underlying_ The address of the underlying asset to be tokenized. - constructor(address hub_, address underlying_) TokenizationSpoke(hub_, underlying_) { + constructor() { _disableInitializers(); } /// @notice Initializer. + /// @param hub_ The address of the associated Hub. + /// @param underlying_ The address of the underlying asset to be tokenized. /// @param shareName The ERC20 name of the share issued by this vault. /// @param shareSymbol The ERC20 symbol of the share issued by this vault. function initialize( + address hub_, + address underlying_, string memory shareName, string memory shareSymbol ) external override reinitializer(SPOKE_REVISION) { - emit SetTokenizationSpokeImmutables(address(HUB), ASSET_ID); + __TokenizationSpoke_init(hub_, underlying_, shareName, shareSymbol); - __TokenizationSpoke_init(shareName, shareSymbol); + emit SetTokenizationSpokeImmutables(address(_hub), _assetId); } } diff --git a/tests/config-engine/BaseConfigEngine.t.sol b/tests/config-engine/BaseConfigEngine.t.sol index 50c8e64c6..7122c3a05 100644 --- a/tests/config-engine/BaseConfigEngine.t.sol +++ b/tests/config-engine/BaseConfigEngine.t.sol @@ -34,6 +34,8 @@ import {HubEngine} from 'src/config-engine/libraries/HubEngine.sol'; import {SpokeEngine} from 'src/config-engine/libraries/SpokeEngine.sol'; import {PositionManagerEngine} from 'src/config-engine/libraries/PositionManagerEngine.sol'; import {TokenizationSpokeDeployer} from 'src/config-engine/libraries/TokenizationSpokeDeployer.sol'; +import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; +import {UpgradeableBeacon} from 'src/dependencies/openzeppelin/UpgradeableBeacon.sol'; import {WETH9} from 'src/dependencies/weth/WETH9.sol'; import {TestnetERC20} from 'tests/helpers/mocks/TestnetERC20.sol'; @@ -99,6 +101,8 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { MockPriceFeed internal priceFeedWbtc; MockPriceFeed internal priceFeedNew; + address internal tokenizationBeacon; + TokenInfo[NUM_TOKENS] internal tokenList; uint256[NUM_TOKENS][NUM_HUBS] internal assetIds; @@ -155,6 +159,11 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { engine = new AaveV4ConfigEngine(); positionManager = new PositionManagerBaseWrapper(address(engine)); + // Deploy the shared TokenizationSpoke implementation and beacon used by tokenization listings. + tokenizationBeacon = address( + new UpgradeableBeacon(address(new TokenizationSpokeInstance()), ADMIN) + ); + _setupRoles(report); vm.label(address(hubs[0]), 'hub1'); @@ -332,7 +341,12 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { liquidityFee: LIQUIDITY_FEE, irStrategy: address(irStrategy1()), irData: IR_DATA, - tokenization: IAaveV4ConfigEngine.TokenizationSpokeConfig({addCap: 0, name: '', symbol: ''}) + tokenization: IAaveV4ConfigEngine.TokenizationSpokeConfig({ + beacon: tokenizationBeacon, + addCap: 0, + name: '', + symbol: '' + }) }); } diff --git a/tests/config-engine/HubEngine.t.sol b/tests/config-engine/HubEngine.t.sol index d3e2094f5..b7f291451 100644 --- a/tests/config-engine/HubEngine.t.sol +++ b/tests/config-engine/HubEngine.t.sol @@ -758,6 +758,7 @@ contract HubEngineTest is BaseConfigEngineTest { IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); listing.underlying = address(newToken); listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + beacon: tokenizationBeacon, addCap: 1000, name: 'Tokenized NEW', symbol: 'tNEW' @@ -770,11 +771,11 @@ contract HubEngineTest is BaseConfigEngineTest { assertEq(config.feeReceiver, FEE_RECEIVER); address predictedProxy = TokenizationSpokeDeployer.computeProxyAddress( + tokenizationBeacon, address(hub1()), address(newToken), 'Tokenized NEW', - 'tNEW', - address(this) + 'tNEW' ); IHub.SpokeConfig memory tsConfig = hub1().getSpokeConfig(assetCountBefore, predictedProxy); @@ -796,17 +797,18 @@ contract HubEngineTest is BaseConfigEngineTest { IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); listing.underlying = address(newToken); listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + beacon: tokenizationBeacon, addCap: 1000, name: 'Tokenized NEW', symbol: 'tNEW' }); address predictedProxy = TokenizationSpokeDeployer.computeProxyAddress( + tokenizationBeacon, address(hub1()), address(newToken), 'Tokenized NEW', - 'tNEW', - address(this) + 'tNEW' ); uint256 assetCountBefore = hub1().getAssetCount(); @@ -820,6 +822,7 @@ contract HubEngineTest is BaseConfigEngineTest { IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); listing.underlying = address(newToken); listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + beacon: tokenizationBeacon, addCap: 1000, name: '', symbol: 'tNEW' @@ -834,11 +837,11 @@ contract HubEngineTest is BaseConfigEngineTest { assertEq(hub1().getSpokeCount(expectedAssetId), 1); address predictedProxy = TokenizationSpokeDeployer.computeProxyAddress( + tokenizationBeacon, address(hub1()), address(newToken), '', - 'tNEW', - address(this) + 'tNEW' ); assertFalse(hub1().isSpokeListed(expectedAssetId, predictedProxy)); assertEq(predictedProxy.code.length, 0); @@ -848,6 +851,7 @@ contract HubEngineTest is BaseConfigEngineTest { IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); listing.underlying = address(newToken); listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + beacon: tokenizationBeacon, addCap: 1000, name: 'Tokenized NEW', symbol: '' @@ -862,11 +866,11 @@ contract HubEngineTest is BaseConfigEngineTest { assertEq(hub1().getSpokeCount(expectedAssetId), 1); address predictedProxy = TokenizationSpokeDeployer.computeProxyAddress( + tokenizationBeacon, address(hub1()), address(newToken), 'Tokenized NEW', - '', - address(this) + '' ); assertFalse(hub1().isSpokeListed(expectedAssetId, predictedProxy)); assertEq(predictedProxy.code.length, 0); @@ -893,8 +897,9 @@ contract HubEngineTest is BaseConfigEngineTest { assertEq(hub2().getAssetCount(), hub2CountBefore + 1); } - function test_computeImplementationAddress() public view { - address predicted = TokenizationSpokeDeployer.computeImplementationAddress( + function test_computeProxyAddress() public view { + address predicted = TokenizationSpokeDeployer.computeProxyAddress( + tokenizationBeacon, address(hub1()), address(newToken), 'Tokenized NEW', @@ -1087,6 +1092,7 @@ contract HubEngineTest is BaseConfigEngineTest { IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); listing.underlying = address(newToken); listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + beacon: tokenizationBeacon, addCap: 1000, name: 'Tokenized NEW', symbol: 'tNEW' diff --git a/tests/contracts/tokenization-spoke/TokenizationSpoke.Config.t.sol b/tests/contracts/tokenization-spoke/TokenizationSpoke.Config.t.sol index 84041ef07..10f454a4b 100644 --- a/tests/contracts/tokenization-spoke/TokenizationSpoke.Config.t.sol +++ b/tests/contracts/tokenization-spoke/TokenizationSpoke.Config.t.sol @@ -2,23 +2,45 @@ pragma solidity ^0.8.0; import 'tests/contracts/tokenization-spoke/TokenizationSpoke.Base.t.sol'; +import {BeaconProxy} from 'src/dependencies/openzeppelin/BeaconProxy.sol'; +import {UpgradeableBeacon} from 'src/dependencies/openzeppelin/UpgradeableBeacon.sol'; contract TokenizationSpokeConfigTest is TokenizationSpokeBaseTest { - function test_constructor_reverts_when_invalid_setup() public { + function test_initialize_reverts_when_invalid_setup() public { + address beacon = _deployTokenizationSpokeBeacon(ADMIN); + address invalidUnderlying = vm.randomAddress(); while (hub1.isUnderlyingListed(invalidUnderlying)) invalidUnderlying = vm.randomAddress(); vm.expectRevert(IHub.AssetNotListed.selector); - new TokenizationSpokeInstance(address(hub1), invalidUnderlying); + new BeaconProxy( + beacon, + abi.encodeCall( + TokenizationSpokeInstance.initialize, + (address(hub1), invalidUnderlying, SHARE_NAME, SHARE_SYMBOL) + ) + ); vm.expectRevert(); - new TokenizationSpokeInstance(address(0), vm.randomAddress()); + new BeaconProxy( + beacon, + abi.encodeCall( + TokenizationSpokeInstance.initialize, + (address(0), vm.randomAddress(), SHARE_NAME, SHARE_SYMBOL) + ) + ); } - function test_constructor_asset_correctly_set() public { + function test_initialize_asset_correctly_set() public { uint256 assetId = vm.randomUint(0, hub1.getAssetCount() - 1); address underlying = hub1.getAsset(assetId).underlying; - TokenizationSpokeInstance instance = new TokenizationSpokeInstance(address(hub1), underlying); + ITokenizationSpoke instance = _deployTokenizationSpoke( + hub1, + underlying, + SHARE_NAME, + SHARE_SYMBOL, + ADMIN + ); assertEq(instance.hub(), address(hub1)); assertEq(instance.assetId(), assetId); assertEq(instance.asset(), underlying); @@ -43,14 +65,13 @@ contract TokenizationSpokeConfigTest is TokenizationSpokeBaseTest { } function test_configuration() public view { - ProxyAdmin proxyAdmin = ProxyAdmin(ProxyHelper.getProxyAdmin(address(daiVault))); - assertEq(proxyAdmin.owner(), ADMIN); - assertEq(proxyAdmin.UPGRADE_INTERFACE_VERSION(), '5.0.0'); + UpgradeableBeacon beacon = UpgradeableBeacon(ProxyHelper.getBeacon(address(daiVault))); + assertEq(beacon.owner(), ADMIN); assertEq( ProxyHelper.getProxyInitializedVersion(address(daiVault)), TokenizationSpokeInstance(address(daiVault)).SPOKE_REVISION() ); - address implementation = ProxyHelper.getImplementation(address(daiVault)); + address implementation = beacon.implementation(); assertEq(ProxyHelper.getProxyInitializedVersion(implementation), type(uint64).max); } } diff --git a/tests/contracts/tokenization-spoke/TokenizationSpoke.Upgradeable.t.sol b/tests/contracts/tokenization-spoke/TokenizationSpoke.Upgradeable.t.sol index b11cd950a..f6976543c 100644 --- a/tests/contracts/tokenization-spoke/TokenizationSpoke.Upgradeable.t.sol +++ b/tests/contracts/tokenization-spoke/TokenizationSpoke.Upgradeable.t.sol @@ -3,9 +3,12 @@ pragma solidity ^0.8.0; import 'tests/contracts/tokenization-spoke/TokenizationSpoke.Base.t.sol'; import {MockTokenizationSpokeInstance} from 'tests/helpers/mocks/MockTokenizationSpokeInstance.sol'; +import {BeaconProxy} from 'src/dependencies/openzeppelin/BeaconProxy.sol'; +import {UpgradeableBeacon} from 'src/dependencies/openzeppelin/UpgradeableBeacon.sol'; +import {ITokenizationSpokeInstance} from 'src/deployments/utils/interfaces/ITokenizationSpokeInstance.sol'; contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { - address internal proxyAdminOwner = makeAddr('proxyAdminOwner'); + address internal beaconOwner = makeAddr('beaconOwner'); function test_implementation_constructor_fuzz(uint64 revision) public { address vaultImplAddress = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); @@ -19,39 +22,29 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { assertEq(ProxyHelper.getProxyInitializedVersion(vaultImplAddress), type(uint64).max); vm.expectRevert(Initializable.InvalidInitialization.selector); - vaultImpl.initialize(SHARE_NAME, SHARE_SYMBOL); + vaultImpl.initialize(address(hub1), address(tokenList.dai), SHARE_NAME, SHARE_SYMBOL); } function test_proxy_constructor_fuzz(uint64 revision) public { revision = uint64(bound(revision, 1, type(uint64).max)); TokenizationSpokeInstance vaultImpl = _deployMockTokenizationSpokeInstance(revision); - address vaultProxyAddress = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); - address proxyAdminAddress = vm.computeCreateAddress(vaultProxyAddress, 1); + UpgradeableBeacon beacon = new UpgradeableBeacon(address(vaultImpl), beaconOwner); + address vaultProxyAddress = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); vm.expectEmit(vaultProxyAddress); - emit IERC1967.Upgraded(address(vaultImpl)); + emit IERC1967.BeaconUpgraded(address(beacon)); vm.expectEmit(vaultProxyAddress); emit ITokenizationSpoke.SetTokenizationSpokeImmutables(address(hub1), daiAssetId); vm.expectEmit(vaultProxyAddress); emit Initializable.Initialized(revision); - vm.expectEmit(proxyAdminAddress); - emit Ownable.OwnershipTransferred(address(0), proxyAdminOwner); - vm.expectEmit(vaultProxyAddress); - emit IERC1967.AdminChanged(address(0), proxyAdminAddress); - ITokenizationSpoke vaultProxy = ITokenizationSpoke( - address( - new TransparentUpgradeableProxy( - address(vaultImpl), - proxyAdminOwner, - abi.encodeCall(TokenizationSpokeInstance.initialize, (SHARE_NAME, SHARE_SYMBOL)) - ) - ) + ITokenizationSpokeInstance vaultProxy = ITokenizationSpokeInstance( + address(new BeaconProxy(address(beacon), _getInitializeCalldata())) ); assertEq(address(vaultProxy), vaultProxyAddress); - assertEq(ProxyHelper.getProxyAdmin(address(vaultProxy)), proxyAdminAddress); - assertEq(ProxyHelper.getImplementation(address(vaultProxy)), address(vaultImpl)); + assertEq(ProxyHelper.getBeacon(address(vaultProxy)), address(beacon)); + assertEq(beacon.implementation(), address(vaultImpl)); assertEq(ProxyHelper.getProxyInitializedVersion(address(vaultProxy)), revision); assertEq(vaultProxy.name(), SHARE_NAME); @@ -61,117 +54,137 @@ contract TokenizationSpokeUpgradeableTest is TokenizationSpokeBaseTest { function test_proxy_reinitialization_fuzz(uint64 initialRevision) public { initialRevision = uint64(bound(initialRevision, 1, type(uint64).max - 1)); TokenizationSpokeInstance vaultImpl = _deployMockTokenizationSpokeInstance(initialRevision); - ITransparentUpgradeableProxy vaultProxy = ITransparentUpgradeableProxy( - address( - new TransparentUpgradeableProxy( - address(vaultImpl), - proxyAdminOwner, - abi.encodeCall(TokenizationSpokeInstance.initialize, (SHARE_NAME, SHARE_SYMBOL)) - ) - ) + UpgradeableBeacon beacon = new UpgradeableBeacon(address(vaultImpl), beaconOwner); + ITokenizationSpokeInstance vaultProxy = ITokenizationSpokeInstance( + address(new BeaconProxy(address(beacon), _getInitializeCalldata())) ); - string memory originalName = ITokenizationSpoke(address(vaultProxy)).name(); + string memory originalName = vaultProxy.name(); uint64 secondRevision = uint64(vm.randomUint(initialRevision + 1, type(uint64).max)); TokenizationSpokeInstance vaultImpl2 = _deployMockTokenizationSpokeInstance(secondRevision); + vm.prank(beaconOwner); + beacon.upgradeTo(address(vaultImpl2)); + string memory newShareName = 'New Share Name'; string memory newShareSymbol = 'New Share Symbol'; vm.expectEmit(address(vaultProxy)); emit ITokenizationSpoke.SetTokenizationSpokeImmutables(address(hub1), daiAssetId); vm.expectEmit(address(vaultProxy)); emit Initializable.Initialized(secondRevision); - vm.recordLogs(); - vm.prank(ProxyHelper.getProxyAdmin(address(vaultProxy))); - vaultProxy.upgradeToAndCall( - address(vaultImpl2), - _getInitializeCalldata(newShareName, newShareSymbol) - ); + vaultProxy.initialize(address(hub1), address(tokenList.dai), newShareName, newShareSymbol); - assertEq(ITokenizationSpoke(address(vaultProxy)).name(), newShareName); - assertEq(ITokenizationSpoke(address(vaultProxy)).symbol(), newShareSymbol); - assertNotEq(ITokenizationSpoke(address(vaultProxy)).name(), originalName); + assertEq(vaultProxy.name(), newShareName); + assertEq(vaultProxy.symbol(), newShareSymbol); + assertNotEq(vaultProxy.name(), originalName); } function test_proxy_constructor_revertsWith_InvalidInitialization_ZeroRevision() public { TokenizationSpokeInstance vaultImpl = _deployMockTokenizationSpokeInstance(0); + UpgradeableBeacon beacon = new UpgradeableBeacon(address(vaultImpl), beaconOwner); vm.expectRevert(Initializable.InvalidInitialization.selector); - new TransparentUpgradeableProxy( - address(vaultImpl), - proxyAdminOwner, - abi.encodeCall(TokenizationSpokeInstance.initialize, (SHARE_NAME, SHARE_SYMBOL)) - ); + new BeaconProxy(address(beacon), _getInitializeCalldata()); } - function test_proxy_constructor_fuzz_revertsWith_InvalidInitialization( + function test_proxy_reinitialization_fuzz_revertsWith_InvalidInitialization( uint64 initialRevision ) public { initialRevision = uint64(bound(initialRevision, 1, type(uint64).max)); TokenizationSpokeInstance vaultImpl = _deployMockTokenizationSpokeInstance(initialRevision); - ITransparentUpgradeableProxy vaultProxy = ITransparentUpgradeableProxy( - address( - new TransparentUpgradeableProxy( - address(vaultImpl), - proxyAdminOwner, - _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) - ) - ) + UpgradeableBeacon beacon = new UpgradeableBeacon(address(vaultImpl), beaconOwner); + ITokenizationSpokeInstance vaultProxy = ITokenizationSpokeInstance( + address(new BeaconProxy(address(beacon), _getInitializeCalldata())) ); + // Re-initializing at the same revision reverts. vm.expectRevert(Initializable.InvalidInitialization.selector); - vm.prank(ProxyHelper.getProxyAdmin(address(vaultProxy))); - vaultProxy.upgradeToAndCall( - address(vaultImpl), - _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) - ); + vaultProxy.initialize(address(hub1), address(tokenList.dai), SHARE_NAME, SHARE_SYMBOL); + // Re-initializing after a downgrade to a lower revision reverts. uint64 secondRevision = uint64(vm.randomUint(0, initialRevision - 1)); TokenizationSpokeInstance vaultImpl2 = _deployMockTokenizationSpokeInstance(secondRevision); + vm.prank(beaconOwner); + beacon.upgradeTo(address(vaultImpl2)); + vm.expectRevert(Initializable.InvalidInitialization.selector); - vm.prank(ProxyHelper.getProxyAdmin(address(vaultProxy))); - vaultProxy.upgradeToAndCall( - address(vaultImpl2), - _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) - ); + vaultProxy.initialize(address(hub1), address(tokenList.dai), SHARE_NAME, SHARE_SYMBOL); } - function test_proxy_reinitialization_revertsWith_CallerNotProxyAdmin() public { + function test_beacon_upgrade_revertsWith_CallerNotOwner() public { TokenizationSpokeInstance vaultImpl = _deployMockTokenizationSpokeInstance(1); - ITransparentUpgradeableProxy vaultProxy = ITransparentUpgradeableProxy( + UpgradeableBeacon beacon = new UpgradeableBeacon(address(vaultImpl), beaconOwner); + new BeaconProxy(address(beacon), _getInitializeCalldata()); + + TokenizationSpokeInstance vaultImpl2 = _deployMockTokenizationSpokeInstance(2); + address notOwner = _makeUser(); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, notOwner)); + vm.prank(notOwner); + beacon.upgradeTo(address(vaultImpl2)); + } + + function test_beacon_upgrade_propagatesImplementationToAllProxies() public { + TokenizationSpokeInstance vaultImpl = _deployMockTokenizationSpokeInstance(1); + UpgradeableBeacon beacon = new UpgradeableBeacon(address(vaultImpl), beaconOwner); + + ITokenizationSpokeInstance daiProxy = ITokenizationSpokeInstance( address( - new TransparentUpgradeableProxy( - address(vaultImpl), - proxyAdminOwner, - _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) + new BeaconProxy( + address(beacon), + abi.encodeCall( + TokenizationSpokeInstance.initialize, + (address(hub1), address(tokenList.dai), 'Core Hub DAI', 'chDAI') + ) ) ) ); - - TokenizationSpokeInstance vaultImpl2 = _deployMockTokenizationSpokeInstance(2); - vm.expectRevert(); - vm.prank(_makeUser()); - vaultProxy.upgradeToAndCall( - address(vaultImpl2), - _getInitializeCalldata(SHARE_NAME, SHARE_SYMBOL) + ITokenizationSpokeInstance usdxProxy = ITokenizationSpokeInstance( + address( + new BeaconProxy( + address(beacon), + abi.encodeCall( + TokenizationSpokeInstance.initialize, + (address(hub1), address(tokenList.usdx), 'Core Hub USDX', 'chUSDX') + ) + ) + ) ); + + assertEq(ProxyHelper.getBeacon(address(daiProxy)), address(beacon)); + assertEq(ProxyHelper.getBeacon(address(usdxProxy)), address(beacon)); + assertEq(beacon.implementation(), address(vaultImpl)); + assertEq(daiProxy.SPOKE_REVISION(), 1); + assertEq(usdxProxy.SPOKE_REVISION(), 1); + + TokenizationSpokeInstance vaultImpl2 = _deployMockTokenizationSpokeInstance(7); + vm.expectEmit(address(beacon)); + emit UpgradeableBeacon.Upgraded(address(vaultImpl2)); + vm.prank(beaconOwner); + beacon.upgradeTo(address(vaultImpl2)); + + assertEq(beacon.implementation(), address(vaultImpl2)); + assertEq(daiProxy.SPOKE_REVISION(), 7); + assertEq(usdxProxy.SPOKE_REVISION(), 7); + + assertEq(daiProxy.asset(), address(tokenList.dai)); + assertEq(daiProxy.symbol(), 'chDAI'); + assertEq(usdxProxy.asset(), address(tokenList.usdx)); + assertEq(usdxProxy.symbol(), 'chUSDX'); } - function _getInitializeCalldata( - string memory shareName, - string memory shareSymbol - ) internal pure returns (bytes memory) { - return abi.encodeCall(TokenizationSpokeInstance.initialize, (shareName, shareSymbol)); + function _getInitializeCalldata() internal view returns (bytes memory) { + return + abi.encodeCall( + TokenizationSpokeInstance.initialize, + (address(hub1), address(tokenList.dai), SHARE_NAME, SHARE_SYMBOL) + ); } function _deployMockTokenizationSpokeInstance( uint64 revision ) internal returns (TokenizationSpokeInstance) { - return - TokenizationSpokeInstance( - address(new MockTokenizationSpokeInstance(revision, address(hub1), address(tokenList.dai))) - ); + return TokenizationSpokeInstance(address(new MockTokenizationSpokeInstance(revision))); } } diff --git a/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol b/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol index efda0711b..a45970df0 100644 --- a/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol +++ b/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol @@ -4,11 +4,14 @@ pragma solidity ^0.8.0; import 'tests/deployments/batches/BatchBase.t.sol'; contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { + AaveV4TokenizationSpokeBeaconBatch public tokenizationSpokeBeaconBatch; AaveV4TokenizationSpokeBatch public tokenizationSpokeBatch; + BatchReports.TokenizationSpokeBeaconBatchReport public beaconReport; BatchReports.TokenizationSpokeBatchReport public report; address public hub; address public irStrategy; + address public beacon; uint256 public assetId; address public underlying; string public shareName = 'Core Hub DAI'; @@ -55,11 +58,16 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { }); vm.stopPrank(); + // Deploy the shared TokenizationSpoke implementation and beacon + tokenizationSpokeBeaconBatch = new AaveV4TokenizationSpokeBeaconBatch(admin, salt); + beaconReport = tokenizationSpokeBeaconBatch.getReport(); + beacon = beaconReport.tokenizationSpokeBeacon; + // Deploy the TokenizationSpoke batch tokenizationSpokeBatch = new AaveV4TokenizationSpokeBatch( + beacon, hub, underlying, - admin, shareName, shareSymbol, salt @@ -67,9 +75,21 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { report = tokenizationSpokeBatch.getReport(); } + function test_getBeaconReport() public view { + assertNotEq(beaconReport.tokenizationSpokeBeacon, address(0)); + assertNotEq(beaconReport.tokenizationSpokeImplementation, address(0)); + assertEq( + ProxyHelper.getImplementation(beaconReport.tokenizationSpokeImplementation), + address(0) + ); + } + function test_getReport() public view { assertNotEq(report.tokenizationSpokeProxy, address(0)); - assertNotEq(report.tokenizationSpokeImplementation, address(0)); + } + + function test_tokenizationSpokeBeacon() public view { + assertEq(ProxyHelper.getBeacon(report.tokenizationSpokeProxy), beacon); } function test_tokenizationSpokeHub() public view { @@ -84,29 +104,41 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { assertEq(ITokenizationSpoke(report.tokenizationSpokeProxy).asset(), underlying); } - function test_revert_zeroHub() public { - vm.expectRevert('invalid hub'); - new AaveV4TokenizationSpokeBatch(address(0), underlying, admin, shareName, shareSymbol, salt); + function test_revert_zeroBeaconOwner() public { + vm.expectRevert('invalid beacon owner'); + new AaveV4TokenizationSpokeBeaconBatch(address(0), keccak256('zeroBeaconOwnerSalt')); } - function test_revert_zeroProxyAdminOwner() public { - vm.expectRevert('invalid proxy admin owner'); + function test_revert_zeroBeacon() public { + vm.expectRevert('invalid beacon'); new AaveV4TokenizationSpokeBatch( + address(0), hub, underlying, + shareName, + shareSymbol, + keccak256('zeroBeaconSalt') + ); + } + + function test_revert_zeroHub() public { + vm.expectRevert('invalid hub'); + new AaveV4TokenizationSpokeBatch( + beacon, address(0), + underlying, shareName, shareSymbol, - keccak256('zeroAdminSalt') + keccak256('zeroHubSalt') ); } function test_revert_emptyShareName() public { vm.expectRevert('invalid share name'); new AaveV4TokenizationSpokeBatch( + beacon, hub, underlying, - admin, '', shareSymbol, keccak256('emptyNameSalt') @@ -116,9 +148,9 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { function test_revert_emptyShareSymbol() public { vm.expectRevert('invalid share symbol'); new AaveV4TokenizationSpokeBatch( + beacon, hub, underlying, - admin, shareName, '', keccak256('emptySymbolSalt') @@ -128,9 +160,9 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { function test_revert_invalidUnderlying() public { vm.expectRevert(); new AaveV4TokenizationSpokeBatch( + beacon, hub, makeAddr('nonExistentUnderlying'), - admin, shareName, shareSymbol, keccak256('invalidAssetSalt') @@ -139,9 +171,9 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { function test_differentSaltProducesDifferentAddress() public { AaveV4TokenizationSpokeBatch newBatch = new AaveV4TokenizationSpokeBatch( + beacon, hub, underlying, - admin, shareName, shareSymbol, keccak256('differentSalt') diff --git a/tests/deployments/batches/BatchBase.t.sol b/tests/deployments/batches/BatchBase.t.sol index 90eedb465..8587a425c 100644 --- a/tests/deployments/batches/BatchBase.t.sol +++ b/tests/deployments/batches/BatchBase.t.sol @@ -16,6 +16,7 @@ import {AaveV4SpokeInstanceBatch} from 'src/deployments/batches/AaveV4SpokeInsta import {AaveV4HubInstanceBatch} from 'src/deployments/batches/AaveV4HubInstanceBatch.sol'; import {AaveV4ConfiguratorBatch} from 'src/deployments/batches/AaveV4ConfiguratorBatch.sol'; import {AaveV4TokenizationSpokeBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBatch.sol'; +import {AaveV4TokenizationSpokeBeaconBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol'; import {AaveV4GatewayBatch} from 'src/deployments/batches/AaveV4GatewayBatch.sol'; import {AaveV4PositionManagerBatch} from 'src/deployments/batches/AaveV4PositionManagerBatch.sol'; import {AaveV4TreasurySpokeBatch} from 'src/deployments/batches/AaveV4TreasurySpokeBatch.sol'; diff --git a/tests/deployments/orchestration/AaveV4TestOrchestration.sol b/tests/deployments/orchestration/AaveV4TestOrchestration.sol index 256fac747..728961ea5 100644 --- a/tests/deployments/orchestration/AaveV4TestOrchestration.sol +++ b/tests/deployments/orchestration/AaveV4TestOrchestration.sol @@ -29,6 +29,7 @@ import {ISpokeInstance} from 'src/deployments/utils/interfaces/ISpokeInstance.so import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {Create2Utils} from 'src/deployments/utils/libraries/Create2Utils.sol'; import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {BeaconProxy} from 'src/dependencies/openzeppelin/BeaconProxy.sol'; library AaveV4TestOrchestration { bool public constant IS_TEST = true; @@ -178,16 +179,22 @@ library AaveV4TestOrchestration { function deployTestTokenizationSpoke( address hub, address underlying, - address proxyAdminOwner, + address beaconOwner, string memory shareName, string memory shareSymbol, bytes32 salt ) external returns (address tokenizationSpokeProxy) { + address beacon = AaveV4DeployBase + .deployTokenizationSpokeBeaconBatch({ + beaconOwner: beaconOwner, + salt: keccak256(abi.encodePacked(salt, 'beacon')) + }) + .tokenizationSpokeBeacon; BatchReports.TokenizationSpokeBatchReport memory report = AaveV4DeployBase .deployTokenizationSpokeBatch({ + beacon: beacon, hub: hub, underlying: underlying, - proxyAdminOwner: proxyAdminOwner, shareName: shareName, shareSymbol: shareSymbol, salt: salt @@ -463,4 +470,8 @@ library AaveV4TestOrchestration { ) internal returns (address) { return address(new TransparentUpgradeableProxy(impl, proxyAdminOwner, initData)); } + + function beaconProxify(address beacon, bytes memory initData) internal returns (address) { + return address(new BeaconProxy(beacon, initData)); + } } diff --git a/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol b/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol index f84272e4c..c053fe1b5 100644 --- a/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol +++ b/tests/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.t.sol @@ -2,10 +2,12 @@ pragma solidity ^0.8.0; import 'tests/deployments/procedures/ProceduresBase.t.sol'; +import {AaveV4TokenizationSpokeBeaconBatch} from 'src/deployments/batches/AaveV4TokenizationSpokeBeaconBatch.sol'; contract AaveV4TokenizationSpokeDeployProcedureTest is ProceduresBase { AaveV4TokenizationSpokeDeployProcedureWrapper public wrapper; address public deployedHub; + address public beacon; uint256 public assetId; address public underlying; string public shareName = 'Test Vault Share'; @@ -15,7 +17,12 @@ contract AaveV4TokenizationSpokeDeployProcedureTest is ProceduresBase { super.setUp(); wrapper = new AaveV4TokenizationSpokeDeployProcedureWrapper(); - // TokenizationSpokeInstance constructor requires hub + // Deploy the shared TokenizationSpoke implementation and beacon + beacon = new AaveV4TokenizationSpokeBeaconBatch(owner, salt) + .getReport() + .tokenizationSpokeBeacon; + + // Hub for the asset listing AaveV4HubInstanceBatch hubInstanceBatch = new AaveV4HubInstanceBatch({ proxyAdminOwner_: admin, authority_: accessManager, @@ -54,78 +61,70 @@ contract AaveV4TokenizationSpokeDeployProcedureTest is ProceduresBase { }); } - function test_deployUpgradeableTokenizationSpokeInstance() public { - (address tokenizationSpokeProxy, address tokenizationSpokeImplementation) = wrapper - .deployUpgradeableTokenizationSpokeInstance( - deployedHub, - underlying, - owner, - shareName, - shareSymbol, - salt - ); - assertNotEq(tokenizationSpokeProxy, address(0)); - assertNotEq(tokenizationSpokeImplementation, address(0)); - assertEq(Ownable(ProxyHelper.getProxyAdmin(tokenizationSpokeProxy)).owner(), owner); - assertEq( - ProxyHelper.getImplementation(tokenizationSpokeProxy), - tokenizationSpokeImplementation + function test_deployTokenizationSpokeInstance() public { + address tokenizationSpokeProxy = wrapper.deployTokenizationSpokeInstance( + beacon, + deployedHub, + underlying, + shareName, + shareSymbol, + salt ); + assertNotEq(tokenizationSpokeProxy, address(0)); + assertEq(ProxyHelper.getBeacon(tokenizationSpokeProxy), beacon); assertEq(ITokenizationSpoke(tokenizationSpokeProxy).hub(), deployedHub); assertEq(ITokenizationSpoke(tokenizationSpokeProxy).assetId(), assetId); assertEq(ITokenizationSpoke(tokenizationSpokeProxy).asset(), underlying); } - function test_deployUpgradeableTokenizationSpokeInstance_reverts() public { - vm.expectRevert('invalid hub'); - wrapper.deployUpgradeableTokenizationSpokeInstance({ - hub: address(0), + function test_deployTokenizationSpokeInstance_reverts() public { + vm.expectRevert('invalid beacon'); + wrapper.deployTokenizationSpokeInstance({ + beacon: address(0), + hub: deployedHub, underlying: underlying, - proxyAdminOwner: owner, shareName: shareName, shareSymbol: shareSymbol, salt: salt }); - vm.expectRevert('invalid proxy admin owner'); - wrapper.deployUpgradeableTokenizationSpokeInstance({ - hub: deployedHub, + vm.expectRevert('invalid hub'); + wrapper.deployTokenizationSpokeInstance({ + beacon: beacon, + hub: address(0), underlying: underlying, - proxyAdminOwner: address(0), shareName: shareName, shareSymbol: shareSymbol, - salt: keccak256('zeroAdminSalt') + salt: keccak256('zeroHubSalt') }); vm.expectRevert('invalid share name'); - wrapper.deployUpgradeableTokenizationSpokeInstance({ + wrapper.deployTokenizationSpokeInstance({ + beacon: beacon, hub: deployedHub, underlying: underlying, - proxyAdminOwner: owner, shareName: '', shareSymbol: shareSymbol, salt: keccak256('emptyNameSalt') }); vm.expectRevert('invalid share symbol'); - wrapper.deployUpgradeableTokenizationSpokeInstance({ + wrapper.deployTokenizationSpokeInstance({ + beacon: beacon, hub: deployedHub, underlying: underlying, - proxyAdminOwner: owner, shareName: shareName, shareSymbol: '', salt: keccak256('emptySymbolSalt') }); } - function test_deployUpgradeableTokenizationSpokeInstance_revertsWith_failedCreate2FactoryCall() - public - { + function test_deployTokenizationSpokeInstance_revertsWith_failedCreate2FactoryCall() public { vm.expectRevert(Create2Utils.FailedCreate2FactoryCall.selector); - wrapper.deployUpgradeableTokenizationSpokeInstance({ + wrapper.deployTokenizationSpokeInstance({ + beacon: beacon, hub: deployedHub, underlying: makeAddr('nonExistentUnderlying'), - proxyAdminOwner: owner, shareName: shareName, shareSymbol: shareSymbol, salt: keccak256('salt') diff --git a/tests/gas/TokenizationSpoke.Operations.gas.t.sol b/tests/gas/TokenizationSpoke.Operations.gas.t.sol index 033ad5764..48ab859fc 100644 --- a/tests/gas/TokenizationSpoke.Operations.gas.t.sol +++ b/tests/gas/TokenizationSpoke.Operations.gas.t.sol @@ -20,7 +20,7 @@ contract TokenizationSpokeOperations_Gas_Tests is Base, TokenizationSpokeHelpers underlying: address(tokenList.dai), shareName: SHARE_NAME, shareSymbol: SHARE_SYMBOL, - proxyAdminOwner: ADMIN + beaconOwner: ADMIN }); _registerTokenizationSpoke({ hub: hub1, diff --git a/tests/helpers/mocks/MockTokenizationSpokeInstance.sol b/tests/helpers/mocks/MockTokenizationSpokeInstance.sol index 6a3a252cb..6327881a9 100644 --- a/tests/helpers/mocks/MockTokenizationSpokeInstance.sol +++ b/tests/helpers/mocks/MockTokenizationSpokeInstance.sol @@ -12,24 +12,21 @@ contract MockTokenizationSpokeInstance is TokenizationSpoke { * @dev Constructor. * @dev It sets the vault spoke revision and disables the initializers. * @param spokeRevision_ The revision of the vault spoke contract. - * @param hub_ The address of the hub. - * @param underlying_ The address of the asset. */ - constructor( - uint64 spokeRevision_, - address hub_, - address underlying_ - ) TokenizationSpoke(hub_, underlying_) { + constructor(uint64 spokeRevision_) { SPOKE_REVISION = spokeRevision_; _disableInitializers(); } /// @inheritdoc TokenizationSpoke function initialize( + address hub_, + address underlying_, string memory shareName, string memory shareSymbol ) external override reinitializer(SPOKE_REVISION) { - emit SetTokenizationSpokeImmutables(address(HUB), ASSET_ID); - __TokenizationSpoke_init(shareName, shareSymbol); + __TokenizationSpoke_init(hub_, underlying_, shareName, shareSymbol); + + emit SetTokenizationSpokeImmutables(address(_hub), _assetId); } } diff --git a/tests/helpers/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol b/tests/helpers/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol index f094f299a..5cf6a6914 100644 --- a/tests/helpers/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol +++ b/tests/helpers/mocks/deployments/procedures/AaveV4TokenizationSpokeDeployProcedureWrapper.sol @@ -6,22 +6,14 @@ import {AaveV4TokenizationSpokeDeployProcedure} from 'src/deployments/procedures contract AaveV4TokenizationSpokeDeployProcedureWrapper is AaveV4TokenizationSpokeDeployProcedure { bool public IS_TEST = true; - function deployUpgradeableTokenizationSpokeInstance( + function deployTokenizationSpokeInstance( + address beacon, address hub, address underlying, - address proxyAdminOwner, string memory shareName, string memory shareSymbol, bytes32 salt - ) external returns (address tokenizationSpokeProxy, address tokenizationSpokeImplementation) { - return - _deployUpgradeableTokenizationSpokeInstance( - hub, - underlying, - proxyAdminOwner, - shareName, - shareSymbol, - salt - ); + ) external returns (address tokenizationSpokeProxy) { + return _deployTokenizationSpokeInstance(beacon, hub, underlying, shareName, shareSymbol, salt); } } diff --git a/tests/helpers/tokenization-spoke/SetupHelpers.sol b/tests/helpers/tokenization-spoke/SetupHelpers.sol index 4085c3178..b96606ab9 100644 --- a/tests/helpers/tokenization-spoke/SetupHelpers.sol +++ b/tests/helpers/tokenization-spoke/SetupHelpers.sol @@ -8,6 +8,7 @@ import {IHub, IHubBase} from 'src/hub/interfaces/IHub.sol'; import {ITreasurySpoke} from 'src/spoke/TreasurySpoke.sol'; import {ITokenizationSpoke} from 'src/spoke/TokenizationSpoke.sol'; import {TokenizationSpokeInstance} from 'src/spoke/instances/TokenizationSpokeInstance.sol'; +import {UpgradeableBeacon} from 'src/dependencies/openzeppelin/UpgradeableBeacon.sol'; import {TestnetERC20} from 'tests/helpers/mocks/TestnetERC20.sol'; import {EIP712Types} from 'tests/helpers/mocks/EIP712Types.sol'; @@ -18,24 +19,41 @@ abstract contract SetupHelpers is SpokeHelpers { // DEPLOY & REGISTER // /////////////////////////////////////////////////////////////////////////////////////////////// + function _deployTokenizationSpokeBeacon( + address beaconOwner + ) internal pausePrank returns (address beacon) { + address tokenizationSpokeImpl = address(new TokenizationSpokeInstance()); + return address(new UpgradeableBeacon(tokenizationSpokeImpl, beaconOwner)); + } + function _deployTokenizationSpoke( IHub hub, address underlying, string memory shareName, string memory shareSymbol, - address proxyAdminOwner + address beaconOwner + ) internal returns (ITokenizationSpoke) { + address beacon = _deployTokenizationSpokeBeacon(beaconOwner); + return _deployTokenizationSpoke(beacon, hub, underlying, shareName, shareSymbol); + } + + function _deployTokenizationSpoke( + address beacon, + IHub hub, + address underlying, + string memory shareName, + string memory shareSymbol ) internal pausePrank returns (ITokenizationSpoke) { - address tokenizationSpokeImpl = address( - new TokenizationSpokeInstance(address(hub), underlying) - ); - ITokenizationSpoke tokenizationSpoke = ITokenizationSpoke( - AaveV4TestOrchestration.proxify( - tokenizationSpokeImpl, - proxyAdminOwner, - abi.encodeCall(TokenizationSpokeInstance.initialize, (shareName, shareSymbol)) - ) - ); - return tokenizationSpoke; + return + ITokenizationSpoke( + AaveV4TestOrchestration.beaconProxify( + beacon, + abi.encodeCall( + TokenizationSpokeInstance.initialize, + (address(hub), underlying, shareName, shareSymbol) + ) + ) + ); } function _registerTokenizationSpoke( diff --git a/tests/utils/ProxyHelper.sol b/tests/utils/ProxyHelper.sol index a7c717ba6..f6b357fee 100644 --- a/tests/utils/ProxyHelper.sol +++ b/tests/utils/ProxyHelper.sol @@ -10,6 +10,8 @@ library ProxyHelper { 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant BEACON_SLOT = + 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; bytes32 internal constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; @@ -18,6 +20,11 @@ library ProxyHelper { return address(uint160(uint256(slotData))); } + function getBeacon(address proxy) internal view returns (address) { + bytes32 slotData = vm.load(proxy, BEACON_SLOT); + return address(uint160(uint256(slotData))); + } + function getImplementation(address proxy) internal view returns (address) { bytes32 slotData = vm.load(proxy, IMPLEMENTATION_SLOT); return address(uint160(uint256(slotData))); From 01b44353c490cad314cfc7dbb68a185d2fcf9efc Mon Sep 17 00:00:00 2001 From: DhairyaSethi <55102840+DhairyaSethi@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:35:08 +0530 Subject: [PATCH 2/2] feat: wire into deployment-engine --- scripts/deploy/AaveV4DeployBatchBase.s.sol | 13 +++++++ .../deploy/examples/AaveV4DeployAnvil.s.sol | 2 ++ src/deployments/README.md | 12 +++++-- .../batches/AaveV4TokenizationSpokeBatch.sol | 4 +-- src/deployments/libraries/BatchReports.sol | 2 ++ .../libraries/OrchestrationReports.sol | 2 ++ .../AaveV4DeployOrchestration.sol | 24 +++++++++++++ src/deployments/utils/MetadataLogger.sol | 10 ++++++ .../utils/libraries/InputUtils.sol | 5 +++ tests/deployments/AaveV4BatchDeployment.t.sol | 34 +++++++++++++++++- .../AaveV4TokenizationSpokeBatch.t.sol | 2 ++ .../fork/PostDeploymentVerificationTest.t.sol | 4 +++ tests/deployments/utils/MetadataLogger.t.sol | 34 ++++++++++++++++++ .../scripts/AaveV4DeployBatchBaseScript.t.sol | 2 ++ tests/utils/BatchTestProcedures.sol | 35 +++++++++++++++++++ 15 files changed, 179 insertions(+), 6 deletions(-) diff --git a/scripts/deploy/AaveV4DeployBatchBase.s.sol b/scripts/deploy/AaveV4DeployBatchBase.s.sol index 2bf98e934..2362d3e03 100644 --- a/scripts/deploy/AaveV4DeployBatchBase.s.sol +++ b/scripts/deploy/AaveV4DeployBatchBase.s.sol @@ -92,6 +92,7 @@ abstract contract AaveV4DeployBatchBaseScript is Script { _logNativeTokenGateway(inputs); _logSignatureGateway(inputs); _logPositionManagers(inputs); + _logTokenizationSpokeBeacon(inputs); _logRoles(inputs); _appendSummary('--------------------------------------------------'); @@ -144,6 +145,10 @@ abstract contract AaveV4DeployBatchBaseScript is Script { _logWarning(string.concat('position manager owner', message, outcome)); sanitizedInputs.positionManagerOwner = deployer; } + if (inputs.deployTokenizationSpokeBeacon && inputs.tokenizationSpokeBeaconOwner == address(0)) { + _logWarning(string.concat('tokenization spoke beacon owner', message, outcome)); + sanitizedInputs.tokenizationSpokeBeaconOwner = deployer; + } if (inputs.salt == bytes32(0)) { _logWarning('salt is zero'); } @@ -199,6 +204,14 @@ abstract contract AaveV4DeployBatchBaseScript is Script { } } + function _logTokenizationSpokeBeacon(InputUtils.FullDeployInputs memory inputs) internal { + if (inputs.deployTokenizationSpokeBeacon) { + _appendSummary('tokenizationSpoke beacon + implementation will be deployed'); + } else { + _appendSummary('tokenizationSpoke beacon: skipped (deployTokenizationSpokeBeacon is false)'); + } + } + function _logRoles(InputUtils.FullDeployInputs memory inputs) internal { if (inputs.grantRoles) { _appendSummary('roles: will be granted during deployment'); diff --git a/scripts/deploy/examples/AaveV4DeployAnvil.s.sol b/scripts/deploy/examples/AaveV4DeployAnvil.s.sol index 279740f7f..dafd4d93b 100644 --- a/scripts/deploy/examples/AaveV4DeployAnvil.s.sol +++ b/scripts/deploy/examples/AaveV4DeployAnvil.s.sol @@ -49,10 +49,12 @@ contract AaveV4DeployAnvil is AaveV4DeployBatchBaseScript { spokeConfiguratorAdmin: address(1), gatewayOwner: address(2), positionManagerOwner: address(3), + tokenizationSpokeBeaconOwner: address(4), nativeWrapper: weth, deployNativeTokenGateway: true, deploySignatureGateway: true, deployPositionManagers: true, + deployTokenizationSpokeBeacon: true, grantRoles: true, hubLabels: hubLabels, spokeLabels: spokeLabels, diff --git a/src/deployments/README.md b/src/deployments/README.md index cf05b3455..5110009d4 100644 --- a/src/deployments/README.md +++ b/src/deployments/README.md @@ -32,13 +32,13 @@ This deploys `LiquidationLogic` via CREATE2 and writes `FOUNDRY_LIBRARIES` to `. make deploy-contracts ``` -This runs `AaveV4DeployOrchestration.deployAaveV4()`, which deploys batches in order: AccessManager → role labeling → Configurators → Configurator role setup → TreasurySpoke → Hubs → Spokes → Gateways → PositionManagers → role grants → DEFAULT_ADMIN transfer. +This runs `AaveV4DeployOrchestration.deployAaveV4()`, which deploys batches in order: AccessManager → role labeling → Configurators → Configurator role setup → TreasurySpoke → Hubs → Spokes → Gateways → PositionManagers → TokenizationSpoke beacon → role grants → DEFAULT_ADMIN transfer. ### TokenizationSpoke -`TokenizationSpoke` is **not** deployed by the orchestration, because it requires an asset to already be listed on a Hub and Spoke. +`TokenizationSpoke` uses a **beacon proxy** pattern: a single shared implementation and `UpgradeableBeacon` are deployed once, and every per-asset `TokenizationSpoke` is a `BeaconProxy` pointing to that beacon. -`TokenizationSpoke` uses a **beacon proxy** pattern: a single shared implementation and `UpgradeableBeacon` are deployed once (via `AaveV4TokenizationSpokeBeaconBatch`), and every per-asset `TokenizationSpoke` is a `BeaconProxy` pointing to that beacon (via `AaveV4TokenizationSpokeBatch`). Because the implementation is generic, the Hub and tokenized asset details are passed to `initialize` rather than baked into the implementation bytecode, so all assets reuse the same implementation. The beacon owner controls upgrades for every `TokenizationSpoke` at once. Each `TokenizationSpoke` proxy should be deployed separately after asset listing, one per asset. +The shared implementation + beacon (`AaveV4TokenizationSpokeBeaconBatch`) are deployed by the orchestration when the `deployTokenizationSpokeBeacon` input is set, and their addresses are written to the deployment JSON as `tokenizationSpokeImplementation` and `tokenizationSpokeBeacon`. Each per-asset `TokenizationSpoke` proxy (`AaveV4TokenizationSpokeBatch`) is **not** deployed by the orchestration — it requires an asset to already be listed on a Hub, so it is deployed separately after asset listing (one per asset), pointing at the shared beacon. ### LiquidationLogic Pre-deployment @@ -239,6 +239,12 @@ AaveV4DeployBatchBase.s.sol (Foundry script entry point) | | Create2Utils.create2Deploy() --> TakerPositionManager | | Create2Utils.create2Deploy() --> ConfigPositionManager | | + | +-- _deployTokenizationSpokeBeaconBatch() (if deployTokenizationSpokeBeacon) + | | AaveV4DeployBase.deployTokenizationSpokeBeaconBatch() + | | new AaveV4TokenizationSpokeBeaconBatch(beaconOwner, salt) + | | Create2Utils.create2Deploy() --> TokenizationSpokeInstance (shared impl) + | | Create2Utils.create2Deploy() --> UpgradeableBeacon + | | | +-- grantRoles (if grantRoles == true) | | _grantHubRoles() (if hubLabels.length > 0) | | AaveV4HubRolesProcedure.grantHubAllRoles() hubAdmin gets roles 101-103 diff --git a/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol b/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol index 0500deb44..b32e4db50 100644 --- a/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol +++ b/src/deployments/batches/AaveV4TokenizationSpokeBatch.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; import {AaveV4TokenizationSpokeDeployProcedure} from 'src/deployments/procedures/deploy/spoke/AaveV4TokenizationSpokeDeployProcedure.sol'; -import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; /// @title AaveV4TokenizationSpokeBatch /// @author Aave Labs @@ -36,7 +35,8 @@ contract AaveV4TokenizationSpokeBatch is AaveV4TokenizationSpokeDeployProcedure }); _report = BatchReports.TokenizationSpokeBatchReport({ - tokenizationSpokeProxy: tokenizationSpokeProxy + tokenizationSpokeProxy: tokenizationSpokeProxy, + tokenizationSpokeBeacon: beacon_ }); } diff --git a/src/deployments/libraries/BatchReports.sol b/src/deployments/libraries/BatchReports.sol index 48bc0ca37..3516e6eb0 100644 --- a/src/deployments/libraries/BatchReports.sol +++ b/src/deployments/libraries/BatchReports.sol @@ -64,7 +64,9 @@ library BatchReports { } /// @dev tokenizationSpokeProxy The deployed TokenizationSpoke beacon proxy contract address. + /// @dev tokenizationSpokeBeacon The UpgradeableBeacon the deployed proxy points to. struct TokenizationSpokeBatchReport { address tokenizationSpokeProxy; + address tokenizationSpokeBeacon; } } diff --git a/src/deployments/libraries/OrchestrationReports.sol b/src/deployments/libraries/OrchestrationReports.sol index bd4d2e76c..e1d498730 100644 --- a/src/deployments/libraries/OrchestrationReports.sol +++ b/src/deployments/libraries/OrchestrationReports.sol @@ -28,6 +28,7 @@ library OrchestrationReports { /// @dev hubInstanceBatchReports Per-hub deployment reports. /// @dev gatewaysBatchReport Gateway deployment report. /// @dev positionManagerBatchReport PositionManager deployment report. + /// @dev tokenizationSpokeBeaconBatchReport TokenizationSpoke shared implementation + beacon deployment report. /// @dev salt The salt used to derive deterministic contract addresses. struct FullDeploymentReport { BatchReports.AuthorityBatchReport authorityBatchReport; @@ -37,6 +38,7 @@ library OrchestrationReports { HubDeploymentReport[] hubInstanceBatchReports; BatchReports.GatewaysBatchReport gatewaysBatchReport; BatchReports.PositionManagerBatchReport positionManagerBatchReport; + BatchReports.TokenizationSpokeBeaconBatchReport tokenizationSpokeBeaconBatchReport; bytes32 salt; } } diff --git a/src/deployments/orchestration/AaveV4DeployOrchestration.sol b/src/deployments/orchestration/AaveV4DeployOrchestration.sol index 55d963730..5294d28a5 100644 --- a/src/deployments/orchestration/AaveV4DeployOrchestration.sol +++ b/src/deployments/orchestration/AaveV4DeployOrchestration.sol @@ -114,6 +114,15 @@ library AaveV4DeployOrchestration { }); } + // Deploy TokenizationSpoke shared implementation + beacon if flag is enabled + if (deployInputs.deployTokenizationSpokeBeacon) { + report.tokenizationSpokeBeaconBatchReport = _deployTokenizationSpokeBeaconBatch({ + logger: logger, + beaconOwner: deployInputs.tokenizationSpokeBeaconOwner, + salt: salt + }); + } + // Set Roles if needed if (deployInputs.grantRoles) { if (deployInputs.hubLabels.length > 0) { @@ -378,6 +387,21 @@ library AaveV4DeployOrchestration { return report; } + function _deployTokenizationSpokeBeaconBatch( + Logger logger, + address beaconOwner, + bytes32 salt + ) internal returns (BatchReports.TokenizationSpokeBeaconBatchReport memory report) { + logger.logHeader1('deploying TokenizationSpokeBeaconBatch'); + report = AaveV4DeployBase.deployTokenizationSpokeBeaconBatch({ + beaconOwner: beaconOwner, + salt: salt + }); + logger.logDetail('TokenizationSpokeImplementation', report.tokenizationSpokeImplementation); + logger.logDetail('TokenizationSpokeBeacon', report.tokenizationSpokeBeacon); + return report; + } + /// @dev Setup roles for the hub and spoke configurators. function _setupConfiguratorRoles( Logger logger, diff --git a/src/deployments/utils/MetadataLogger.sol b/src/deployments/utils/MetadataLogger.sol index 017fefa80..4b0a8fb37 100644 --- a/src/deployments/utils/MetadataLogger.sol +++ b/src/deployments/utils/MetadataLogger.sol @@ -82,5 +82,15 @@ contract MetadataLogger is Logger { if (report.positionManagerBatchReport.configPositionManager != address(0)) { _write('configPositionManager', report.positionManagerBatchReport.configPositionManager); } + if (report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon != address(0)) { + _write( + 'tokenizationSpokeBeacon', + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon + ); + _write( + 'tokenizationSpokeImplementation', + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation + ); + } } } diff --git a/src/deployments/utils/libraries/InputUtils.sol b/src/deployments/utils/libraries/InputUtils.sol index da21a62e0..dd28df525 100644 --- a/src/deployments/utils/libraries/InputUtils.sol +++ b/src/deployments/utils/libraries/InputUtils.sol @@ -16,10 +16,13 @@ library InputUtils { /// @dev spokeConfiguratorAdmin The admin granted all spoke configurator roles. Only used when grantRoles is true. /// @dev gatewayOwner The owner of the native token and signature gateways. /// @dev positionManagerOwner The owner of the position manager contracts (giver/taker/config). + /// @dev tokenizationSpokeBeaconOwner The owner of the shared TokenizationSpoke beacon (upgrade authority). + /// Only used when deployTokenizationSpokeBeacon is true. /// @dev nativeWrapper The address of the native wrapper (required when deployNativeTokenGateway is true). /// @dev deployNativeTokenGateway Whether to deploy the NativeTokenGateway. /// @dev deploySignatureGateway Whether to deploy the SignatureGateway. /// @dev deployPositionManagers Whether to deploy the position manager batch (giver/taker/config). + /// @dev deployTokenizationSpokeBeacon Whether to deploy the shared TokenizationSpoke implementation and beacon. /// @dev grantRoles Whether to grant roles during deployment. When `false`, only deploy-time ownership /// addresses (proxyAdminOwner, treasurySpokeOwner) are set, defaulting /// to the deployer. The deployer also retains the AccessManager ACCESS_MANAGER_ADMIN_ROLE. @@ -38,10 +41,12 @@ library InputUtils { address spokeConfiguratorAdmin; address gatewayOwner; address positionManagerOwner; + address tokenizationSpokeBeaconOwner; address nativeWrapper; bool deployNativeTokenGateway; bool deploySignatureGateway; bool deployPositionManagers; + bool deployTokenizationSpokeBeacon; bool grantRoles; string[] hubLabels; string[] spokeLabels; diff --git a/tests/deployments/AaveV4BatchDeployment.t.sol b/tests/deployments/AaveV4BatchDeployment.t.sol index a1870f64f..89dc57d52 100644 --- a/tests/deployments/AaveV4BatchDeployment.t.sol +++ b/tests/deployments/AaveV4BatchDeployment.t.sol @@ -17,10 +17,12 @@ contract AaveV4BatchDeploymentTest is BatchTestProcedures { spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), gatewayOwner: makeAddr('gatewayOwner'), positionManagerOwner: makeAddr('positionManagerOwner'), + tokenizationSpokeBeaconOwner: makeAddr('tokenizationSpokeBeaconOwner'), nativeWrapper: _weth9, deployNativeTokenGateway: true, deploySignatureGateway: true, deployPositionManagers: true, + deployTokenizationSpokeBeacon: true, grantRoles: true, hubLabels: _hubLabels, spokeLabels: _spokeLabels, @@ -234,6 +236,28 @@ contract AaveV4BatchDeploymentTest is BatchTestProcedures { checkedV4Deployment(); } + function testAaveV4BatchDeployment_withoutTokenizationSpokeBeacon() public { + _inputs.deployTokenizationSpokeBeacon = false; + checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroTokenizationSpokeBeaconOwner_withBeacon_reverts() + public + { + _inputs.tokenizationSpokeBeaconOwner = address(0); + _inputs.deployTokenizationSpokeBeacon = true; + + vm.expectRevert('invalid beacon owner'); + this.checkedV4Deployment(); + } + + function testAaveV4BatchDeployment_withZeroTokenizationSpokeBeaconOwner_withoutBeacon() public { + _inputs.tokenizationSpokeBeaconOwner = address(0); + _inputs.deployTokenizationSpokeBeacon = false; + + checkedV4Deployment(); + } + function testAaveV4BatchDeployment_withDuplicateHubLabels_reverts() public { _inputs.hubLabels = new string[](3); _inputs.hubLabels[0] = 'core'; @@ -453,7 +477,8 @@ contract AaveV4BatchDeploymentTest is BatchTestProcedures { /// 5. Spokes (proxyAdminOwner) /// 6. Gateways (gatewayOwner, nativeWrapper) /// 7. PositionManagers (positionManagerOwner) - /// 8. Roles (hubAdmin, hubConfiguratorAdmin, spokeAdmin, spokeConfiguratorAdmin, accessManagerAdmin) + /// 8. TokenizationSpoke beacon (tokenizationSpokeBeaconOwner) + /// 9. Roles (hubAdmin, hubConfiguratorAdmin, spokeAdmin, spokeConfiguratorAdmin, accessManagerAdmin) function _getExpectedError() internal view @@ -492,6 +517,13 @@ contract AaveV4BatchDeploymentTest is BatchTestProcedures { return (true, bytes('invalid owner')); } + // 6. tokenization spoke beacon requires owner when deployed + if ( + _inputs.deployTokenizationSpokeBeacon && _inputs.tokenizationSpokeBeaconOwner == address(0) + ) { + return (true, bytes('invalid beacon owner')); + } + if (_inputs.grantRoles) { bool hasHubs = _inputs.hubLabels.length > 0; bool hasSpokes = _inputs.spokeLabels.length > 0; diff --git a/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol b/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol index a45970df0..fe494bf27 100644 --- a/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol +++ b/tests/deployments/batches/AaveV4TokenizationSpokeBatch.t.sol @@ -86,9 +86,11 @@ contract AaveV4TokenizationSpokeBatchTest is BatchBaseTest { function test_getReport() public view { assertNotEq(report.tokenizationSpokeProxy, address(0)); + assertEq(report.tokenizationSpokeBeacon, beacon); } function test_tokenizationSpokeBeacon() public view { + assertEq(report.tokenizationSpokeBeacon, beacon); assertEq(ProxyHelper.getBeacon(report.tokenizationSpokeProxy), beacon); } diff --git a/tests/deployments/fork/PostDeploymentVerificationTest.t.sol b/tests/deployments/fork/PostDeploymentVerificationTest.t.sol index 69daaa5bf..682f2cf74 100644 --- a/tests/deployments/fork/PostDeploymentVerificationTest.t.sol +++ b/tests/deployments/fork/PostDeploymentVerificationTest.t.sol @@ -207,12 +207,14 @@ contract PostDeploymentVerificationTest is PostDeploymentVerificationBase, AaveV spokeConfiguratorAdmin: params.spokeConfiguratorAdmin, gatewayOwner: params.gatewayOwner, positionManagerOwner: params.positionManagerOwner, + tokenizationSpokeBeaconOwner: address(0), nativeWrapper: (params.deployNativeTokenGateway && params.useValidNativeWrapper) ? weth : address(0), deployNativeTokenGateway: params.deployNativeTokenGateway, deploySignatureGateway: params.deploySignatureGateway, deployPositionManagers: params.deployPositionManagers, + deployTokenizationSpokeBeacon: false, grantRoles: params.grantRoles, hubLabels: hubLabels, spokeLabels: spokeLabels, @@ -261,10 +263,12 @@ contract PostDeploymentVerificationTest is PostDeploymentVerificationBase, AaveV spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), gatewayOwner: makeAddr('gatewayOwner'), positionManagerOwner: makeAddr('positionManagerOwner'), + tokenizationSpokeBeaconOwner: address(0), nativeWrapper: weth, deployNativeTokenGateway: true, deploySignatureGateway: true, deployPositionManagers: true, + deployTokenizationSpokeBeacon: false, grantRoles: true, hubLabels: hubLabels, spokeLabels: spokeLabels, diff --git a/tests/deployments/utils/MetadataLogger.t.sol b/tests/deployments/utils/MetadataLogger.t.sol index 12e4f536f..32050204c 100644 --- a/tests/deployments/utils/MetadataLogger.t.sol +++ b/tests/deployments/utils/MetadataLogger.t.sol @@ -45,6 +45,13 @@ contract MetadataLoggerTest is Test { report.positionManagerBatchReport.giverPositionManager = makeAddr('giverPM'); report.positionManagerBatchReport.takerPositionManager = makeAddr('takerPM'); report.positionManagerBatchReport.configPositionManager = makeAddr('configPM'); + + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon = makeAddr( + 'tokenizationSpokeBeacon' + ); + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation = makeAddr( + 'tokenizationSpokeImpl' + ); } function test_writeJsonReportMarket_fullReport() public { @@ -132,6 +139,29 @@ contract MetadataLoggerTest is Test { vm.parseJsonAddress(json, '$.configPositionManager'), report.positionManagerBatchReport.configPositionManager ); + + // TokenizationSpoke beacon + implementation + assertEq( + vm.parseJsonAddress(json, '$.tokenizationSpokeBeacon'), + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon + ); + assertEq( + vm.parseJsonAddress(json, '$.tokenizationSpokeImplementation'), + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation + ); + } + + function test_writeJsonReportMarket_noTokenizationSpokeBeacon() public { + MetadataLogger logger = new MetadataLogger(OUTPUT_DIR); + OrchestrationReports.FullDeploymentReport memory report = _fullReport(); + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon = address(0); + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation = address(0); + + logger.writeJsonReportMarket(report); + string memory json = logger.getJson(); + + assertFalse(vm.keyExistsJson(json, '$.tokenizationSpokeBeacon')); + assertFalse(vm.keyExistsJson(json, '$.tokenizationSpokeImplementation')); } function test_writeJsonReportMarket_noGateways() public { @@ -175,6 +205,8 @@ contract MetadataLoggerTest is Test { report.positionManagerBatchReport.giverPositionManager = address(0); report.positionManagerBatchReport.takerPositionManager = address(0); report.positionManagerBatchReport.configPositionManager = address(0); + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon = address(0); + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation = address(0); logger.writeJsonReportMarket(report); string memory json = logger.getJson(); @@ -185,6 +217,8 @@ contract MetadataLoggerTest is Test { assertFalse(vm.keyExistsJson(json, '$.giverPositionManager')); assertFalse(vm.keyExistsJson(json, '$.takerPositionManager')); assertFalse(vm.keyExistsJson(json, '$.configPositionManager')); + assertFalse(vm.keyExistsJson(json, '$.tokenizationSpokeBeacon')); + assertFalse(vm.keyExistsJson(json, '$.tokenizationSpokeImplementation')); // Core fields present assertEq( diff --git a/tests/scripts/AaveV4DeployBatchBaseScript.t.sol b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol index 60b769d01..db119ae14 100644 --- a/tests/scripts/AaveV4DeployBatchBaseScript.t.sol +++ b/tests/scripts/AaveV4DeployBatchBaseScript.t.sol @@ -60,10 +60,12 @@ contract AaveV4DeployBatchBaseScriptTest is Test { spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'), gatewayOwner: makeAddr('gatewayOwner'), positionManagerOwner: makeAddr('positionManagerOwner'), + tokenizationSpokeBeaconOwner: makeAddr('tokenizationSpokeBeaconOwner'), nativeWrapper: address(new WETH9()), deployNativeTokenGateway: true, deploySignatureGateway: true, deployPositionManagers: true, + deployTokenizationSpokeBeacon: true, grantRoles: true, hubLabels: _toArray('hub1', 'hub2', 'hub3'), spokeLabels: _toArray('spoke1', 'spoke2', 'spoke3'), diff --git a/tests/utils/BatchTestProcedures.sol b/tests/utils/BatchTestProcedures.sol index ed33f852e..9d7f8e73f 100644 --- a/tests/utils/BatchTestProcedures.sol +++ b/tests/utils/BatchTestProcedures.sol @@ -162,6 +162,9 @@ contract BatchTestProcedures is Test, Create2TestHelper, WETHDeployProcedure { inputs.positionManagerOwner = inputs.positionManagerOwner != address(0) ? inputs.positionManagerOwner : _deployer; + inputs.tokenizationSpokeBeaconOwner = inputs.tokenizationSpokeBeaconOwner != address(0) + ? inputs.tokenizationSpokeBeaconOwner + : _deployer; // Sync parallel arrays with spokeLabels length inputs.hubLabels = _hubLabels; @@ -171,6 +174,7 @@ contract BatchTestProcedures is Test, Create2TestHelper, WETHDeployProcedure { inputs.deployNativeTokenGateway = true; inputs.deploySignatureGateway = true; inputs.deployPositionManagers = true; + inputs.deployTokenizationSpokeBeacon = true; return inputs; } @@ -232,6 +236,30 @@ contract BatchTestProcedures is Test, Create2TestHelper, WETHDeployProcedure { ); } + if (inputs.deployTokenizationSpokeBeacon) { + assertNotEq( + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon, + address(0), + 'TokenizationSpokeBeacon' + ); + assertNotEq( + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation, + address(0), + 'TokenizationSpokeImplementation' + ); + } else { + assertEq( + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon, + address(0), + 'Zero TokenizationSpokeBeacon' + ); + assertEq( + report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeImplementation, + address(0), + 'Zero TokenizationSpokeImplementation' + ); + } + assertNotEq(report.authorityBatchReport.accessManager, address(0), 'AccessManager'); assertNotEq(report.configuratorBatchReport.spokeConfigurator, address(0), 'SpokeConfigurator'); assertNotEq(report.configuratorBatchReport.hubConfigurator, address(0), 'HubConfigurator'); @@ -852,6 +880,13 @@ contract BatchTestProcedures is Test, Create2TestHelper, WETHDeployProcedure { 'ConfigPositionManager owner' ); } + if (inputs.deployTokenizationSpokeBeacon) { + assertEq( + Ownable(report.tokenizationSpokeBeaconBatchReport.tokenizationSpokeBeacon).owner(), + inputs.tokenizationSpokeBeaconOwner, + 'TokenizationSpokeBeacon owner' + ); + } } function _etchSetup() internal {