diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml index eb90db07b..f29cc5fc0 100644 --- a/.github/workflows/comment.yml +++ b/.github/workflows/comment.yml @@ -16,6 +16,6 @@ permissions: jobs: comment: - uses: bgd-labs/github-workflows/.github/workflows/comment.yml@main + uses: aave-dao/github-workflows/.github/workflows/comment.yml@main secrets: READ_ONLY_PAT: ${{ secrets.READ_ONLY_PAT }} diff --git a/.github/workflows/tests-merge.yml b/.github/workflows/tests-merge.yml index af755eeda..0cb529db5 100644 --- a/.github/workflows/tests-merge.yml +++ b/.github/workflows/tests-merge.yml @@ -9,7 +9,7 @@ on: jobs: lint: name: Prettier lint check - uses: bgd-labs/github-workflows/.github/workflows/foundry-lint-prettier.yml@main + uses: aave-dao/github-workflows/.github/workflows/foundry-lint-prettier.yml@main test: name: Foundry build n test runs-on: ubuntu-latest @@ -20,15 +20,15 @@ jobs: token: ${{ secrets.READ_ONLY_PAT || github.token }} - name: Run Foundry setup - uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + uses: aave-dao/github-workflows/.github/actions/foundry-setup@main with: FOUNDRY_VERSION: stable - name: Run Forge size - uses: bgd-labs/github-workflows/.github/actions/foundry-size@main + uses: aave-dao/github-workflows/.github/actions/foundry-size@main - name: Run Forge tests id: test - uses: bgd-labs/github-workflows/.github/actions/foundry-test@main + uses: aave-dao/github-workflows/.github/actions/foundry-test@main with: FOUNDRY_PROFILE: ci diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml index cb1ff90f1..498874e4c 100644 --- a/.github/workflows/tests-pr.yml +++ b/.github/workflows/tests-pr.yml @@ -6,7 +6,7 @@ on: jobs: lint: name: Prettier lint check - uses: bgd-labs/github-workflows/.github/workflows/foundry-lint-prettier.yml@main + uses: aave-dao/github-workflows/.github/workflows/foundry-lint-prettier.yml@main test: name: Foundry build n test runs-on: ubuntu-latest @@ -17,23 +17,23 @@ jobs: token: ${{ secrets.READ_ONLY_PAT || github.token }} - name: Run Foundry setup - uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main + uses: aave-dao/github-workflows/.github/actions/foundry-setup@main with: FOUNDRY_VERSION: stable - name: Run Forge size - uses: bgd-labs/github-workflows/.github/actions/foundry-size@main + uses: aave-dao/github-workflows/.github/actions/foundry-size@main - name: Run Gas report - uses: bgd-labs/github-workflows/.github/actions/foundry-gas-report@main + uses: aave-dao/github-workflows/.github/actions/foundry-gas-report@main - name: Run Forge tests - uses: bgd-labs/github-workflows/.github/actions/foundry-test@main + uses: aave-dao/github-workflows/.github/actions/foundry-test@main with: FOUNDRY_PROFILE: pr FORGE_SNAPSHOT_CHECK: true - name: Upload & Trigger Comment artifact - uses: bgd-labs/github-workflows/.github/actions/comment-artifact@main + uses: aave-dao/github-workflows/.github/actions/comment-artifact@main with: nameSuffix: "default" diff --git a/src/addresses-provider/AddressesProvider.sol b/src/addresses-provider/AddressesProvider.sol new file mode 100644 index 000000000..4394f4f99 --- /dev/null +++ b/src/addresses-provider/AddressesProvider.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity 0.8.28; + +import {Ownable2StepUpgradeable} from 'src/dependencies/openzeppelin-upgradeable/Ownable2StepUpgradeable.sol'; +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; +import {AddressesProviderStorage} from 'src/addresses-provider/AddressesProviderStorage.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; + +/// @title AddressesProvider +/// @author Aave Labs +/// @notice Main registry of Aave V4 contract addresses. +abstract contract AddressesProvider is + AddressesProviderStorage, + Ownable2StepUpgradeable, + IAddressesProvider +{ + using EnumerableSet for *; + + /// @inheritdoc IAddressesProvider + string public constant CANONICAL_HUB_TAG = 'CANONICAL_HUB'; + + /// @inheritdoc IAddressesProvider + string public constant CANONICAL_SPOKE_TAG = 'CANONICAL_SPOKE'; + + /// @inheritdoc IAddressesProvider + string public constant TOKENIZATION_SPOKE_TAG = 'TOKENIZATION_SPOKE'; + + /// @inheritdoc IAddressesProvider + string public constant TREASURY_SPOKE_TAG = 'TREASURY_SPOKE'; + + /// @dev To be overridden by the inheriting AddressesProvider instance contract. + function initialize(address owner) external virtual; + + /// @inheritdoc IAddressesProvider + function setEntry( + string calldata name, + string calldata tag, + address newAddress + ) external onlyOwner { + _setEntry({name: name, tag: tag, newAddress: newAddress}); + } + + /// @inheritdoc IAddressesProvider + function setCanonicalHub(string calldata name, address hub) external onlyOwner { + _setEntry({name: name, tag: CANONICAL_HUB_TAG, newAddress: hub}); + } + + /// @inheritdoc IAddressesProvider + function setCanonicalSpoke(string calldata name, address spoke) external onlyOwner { + _setEntry({name: name, tag: CANONICAL_SPOKE_TAG, newAddress: spoke}); + } + + /// @inheritdoc IAddressesProvider + function setTokenizationSpoke(string calldata name, address spoke) external onlyOwner { + _setEntry({name: name, tag: TOKENIZATION_SPOKE_TAG, newAddress: spoke}); + } + + /// @inheritdoc IAddressesProvider + function setTreasurySpoke(string calldata name, address spoke) external onlyOwner { + _setEntry({name: name, tag: TREASURY_SPOKE_TAG, newAddress: spoke}); + } + + /// @inheritdoc IAddressesProvider + function getAddress(bytes32 id) external view returns (address) { + return _idToEntry[id].addr; + } + + /// @inheritdoc IAddressesProvider + function getAddress(string calldata name, string calldata tag) external view returns (address) { + return _getAddress({name: name, tag: tag}); + } + + /// @inheritdoc IAddressesProvider + function getEntry(bytes32 id) external view returns (Entry memory) { + return _idToEntry[id]; + } + + /// @inheritdoc IAddressesProvider + function getTagCount() external view returns (uint256) { + return _tagsSet.length(); + } + + /// @inheritdoc IAddressesProvider + function getTags() external view returns (string[] memory) { + return _tagsSet.values(); + } + + /// @inheritdoc IAddressesProvider + function getTags(uint256 start, uint256 end) external view returns (string[] memory) { + return _tagsSet.values(start, end); + } + + /// @inheritdoc IAddressesProvider + function getIdCount(string calldata tag) external view returns (uint256) { + return _tagToIdSet[tag].length(); + } + + /// @inheritdoc IAddressesProvider + function getIds(string calldata tag) external view returns (bytes32[] memory) { + return _tagToIdSet[tag].values(); + } + + /// @inheritdoc IAddressesProvider + function getIds( + string calldata tag, + uint256 start, + uint256 end + ) external view returns (bytes32[] memory) { + return _tagToIdSet[tag].values(start, end); + } + + /// @inheritdoc IAddressesProvider + function getAddresses(string calldata tag) external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[tag].values()); + } + + /// @inheritdoc IAddressesProvider + function getAddresses( + string calldata tag, + uint256 start, + uint256 end + ) external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[tag].values(start, end)); + } + + /// @inheritdoc IAddressesProvider + function getAddressIdCount(address addr) external view returns (uint256) { + return _addressToIdSet[addr].length(); + } + + /// @inheritdoc IAddressesProvider + function getAddressIds(address addr) external view returns (bytes32[] memory) { + return _addressToIdSet[addr].values(); + } + + /// @inheritdoc IAddressesProvider + function getAddressIds( + address addr, + uint256 start, + uint256 end + ) external view returns (bytes32[] memory) { + return _addressToIdSet[addr].values(start, end); + } + + /// @inheritdoc IAddressesProvider + function getEntries(address addr) external view returns (Entry[] memory) { + return _toEntries(_addressToIdSet[addr].values()); + } + + /// @inheritdoc IAddressesProvider + function getEntries( + address addr, + uint256 start, + uint256 end + ) external view returns (Entry[] memory) { + return _toEntries(_addressToIdSet[addr].values(start, end)); + } + + /// @inheritdoc IAddressesProvider + function isRegistered(address addr, string calldata tag) external view returns (bool) { + return _addressToTagCount[addr][keccak256(bytes(tag))] > 0; + } + + /// @inheritdoc IAddressesProvider + function getCanonicalHub(string calldata name) external view returns (address) { + return _getAddress({name: name, tag: CANONICAL_HUB_TAG}); + } + + /// @inheritdoc IAddressesProvider + function getCanonicalHubs() external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[CANONICAL_HUB_TAG].values()); + } + + /// @inheritdoc IAddressesProvider + function getCanonicalHubs(uint256 start, uint256 end) external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[CANONICAL_HUB_TAG].values(start, end)); + } + + /// @inheritdoc IAddressesProvider + function getCanonicalSpoke(string calldata name) external view returns (address) { + return _getAddress({name: name, tag: CANONICAL_SPOKE_TAG}); + } + + /// @inheritdoc IAddressesProvider + function getCanonicalSpokes() external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[CANONICAL_SPOKE_TAG].values()); + } + + /// @inheritdoc IAddressesProvider + function getCanonicalSpokes(uint256 start, uint256 end) external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[CANONICAL_SPOKE_TAG].values(start, end)); + } + + /// @inheritdoc IAddressesProvider + function getTokenizationSpoke(string calldata name) external view returns (address) { + return _getAddress({name: name, tag: TOKENIZATION_SPOKE_TAG}); + } + + /// @inheritdoc IAddressesProvider + function getTokenizationSpokes() external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[TOKENIZATION_SPOKE_TAG].values()); + } + + /// @inheritdoc IAddressesProvider + function getTokenizationSpokes( + uint256 start, + uint256 end + ) external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[TOKENIZATION_SPOKE_TAG].values(start, end)); + } + + /// @inheritdoc IAddressesProvider + function getTreasurySpoke(string calldata name) external view returns (address) { + return _getAddress({name: name, tag: TREASURY_SPOKE_TAG}); + } + + /// @inheritdoc IAddressesProvider + function getTreasurySpokes() external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[TREASURY_SPOKE_TAG].values()); + } + + /// @inheritdoc IAddressesProvider + function getTreasurySpokes(uint256 start, uint256 end) external view returns (address[] memory) { + return _toAddresses(_tagToIdSet[TREASURY_SPOKE_TAG].values(start, end)); + } + + /// @inheritdoc IAddressesProvider + function getId(string calldata name, string calldata tag) external pure returns (bytes32) { + return _getId({name: name, tag: tag}); + } + + function _setEntry(string memory name, string memory tag, address newAddress) internal { + require(bytes(name).length > 0, InvalidName()); + require(bytes(tag).length > 0, InvalidTag()); + + bytes32 id = _getId({name: name, tag: tag}); + Entry memory oldEntry = _idToEntry[id]; + + if (newAddress == address(0)) { + require(oldEntry.addr != address(0), AddressNotSet(id)); + _tagToIdSet[oldEntry.tag].remove(id); + if (_tagToIdSet[oldEntry.tag].length() == 0) { + _tagsSet.remove(oldEntry.tag); + } + _addressToIdSet[oldEntry.addr].remove(id); + _addressToTagCount[oldEntry.addr][keccak256(bytes(oldEntry.tag))]--; + delete _idToEntry[id]; + } else { + require(oldEntry.addr == address(0), AddressAlreadySet(id)); + _idToEntry[id] = Entry({name: name, tag: tag, addr: newAddress}); + _tagToIdSet[tag].add(id); + _tagsSet.add(tag); + _addressToIdSet[newAddress].add(id); + _addressToTagCount[newAddress][keccak256(bytes(tag))]++; + } + + emit SetEntry(id, name, tag, oldEntry.addr, newAddress); + } + + function _getAddress(string memory name, string memory tag) internal view returns (address) { + return _idToEntry[_getId({name: name, tag: tag})].addr; + } + + function _toAddresses(bytes32[] memory ids) internal view returns (address[] memory) { + address[] memory addresses = new address[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + addresses[i] = _idToEntry[ids[i]].addr; + } + return addresses; + } + + function _toEntries(bytes32[] memory ids) internal view returns (Entry[] memory) { + Entry[] memory entries = new Entry[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + entries[i] = _idToEntry[ids[i]]; + } + return entries; + } + + function _getId(string memory name, string memory tag) internal pure returns (bytes32) { + return keccak256(abi.encode(name, tag)); + } +} diff --git a/src/addresses-provider/AddressesProviderStorage.sol b/src/addresses-provider/AddressesProviderStorage.sol new file mode 100644 index 000000000..effd0ebd2 --- /dev/null +++ b/src/addresses-provider/AddressesProviderStorage.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity 0.8.28; + +import {EnumerableSet} from 'src/dependencies/openzeppelin/EnumerableSet.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; + +/// @title AddressesProviderStorage +/// @author Aave Labs +/// @notice Storage layout for the AddressesProvider contract. +/// @dev This contract defines all storage variables used by the AddressesProvider. +abstract contract AddressesProviderStorage { + /// @dev Map of entry identifiers to their respective entries. + mapping(bytes32 id => IAddressesProvider.Entry) internal _idToEntry; + + /// @dev Map of tags to their respective sets of entry identifiers. + mapping(string tag => EnumerableSet.Bytes32Set) internal _tagToIdSet; + + /// @dev Set of all tags. + /// @dev A tag is included in the set only if it has at least one registered entry. + EnumerableSet.StringSet internal _tagsSet; + + /// @dev Map of registered addresses to their respective sets of entry identifiers. + /// @dev An address may be registered under more than one entry. + mapping(address addr => EnumerableSet.Bytes32Set) internal _addressToIdSet; + + /// @dev Map of registered addresses and tag hashes to their respective number of entries. + mapping(address addr => mapping(bytes32 tagHash => uint256 count)) internal _addressToTagCount; + + /// @dev Reserved storage space to allow for future layout updates. + uint256[50] private __gap; +} diff --git a/src/addresses-provider/instances/AddressesProviderInstance.sol b/src/addresses-provider/instances/AddressesProviderInstance.sol new file mode 100644 index 000000000..0ab6c71d6 --- /dev/null +++ b/src/addresses-provider/instances/AddressesProviderInstance.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity 0.8.28; + +import {AddressesProvider} from 'src/addresses-provider/AddressesProvider.sol'; + +/// @title AddressesProviderInstance +/// @author Aave Labs +/// @notice Implementation contract for the AddressesProvider. +contract AddressesProviderInstance is AddressesProvider { + uint64 public constant ADDRESSES_PROVIDER_REVISION = 1; + + /// @dev Constructor. + constructor() { + _disableInitializers(); + } + + /// @notice Initializer. + /// @param owner The address of the owner. + function initialize(address owner) external override reinitializer(ADDRESSES_PROVIDER_REVISION) { + __Ownable_init(owner); + __Ownable2Step_init(); + } +} diff --git a/src/addresses-provider/interfaces/IAddressesProvider.sol b/src/addresses-provider/interfaces/IAddressesProvider.sol new file mode 100644 index 000000000..10a64b729 --- /dev/null +++ b/src/addresses-provider/interfaces/IAddressesProvider.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +/// @title IAddressesProvider +/// @author Aave Labs +/// @notice Interface for the AddressesProvider. +interface IAddressesProvider { + /// @notice Entry registered under an identifier. + /// @param name The name of the entry. + /// @param tag The tag grouping the entry. + /// @param addr The registered address. + struct Entry { + string name; + string tag; + address addr; + } + + /// @notice Emitted when the address of an entry is updated. + /// @param id The identifier of the entry. + /// @param name The name of the entry. + /// @param tag The tag grouping the entry. + /// @param oldAddress The previous address of the entry. + /// @param newAddress The new address of the entry. + event SetEntry( + bytes32 indexed id, + string name, + string tag, + address indexed oldAddress, + address indexed newAddress + ); + + /// @notice Thrown when the specified tag is invalid. + error InvalidTag(); + + /// @notice Thrown when the specified name is invalid. + error InvalidName(); + + /// @notice Thrown when an address is already registered under the identifier. + error AddressAlreadySet(bytes32 id); + + /// @notice Thrown when no address is registered under the identifier. + error AddressNotSet(bytes32 id); + + /// @notice Associates an address with a name, grouped under a tag. + /// @dev Associating the zero address removes the entry and its identifier from enumeration, it reverts if no address is registered. + /// @dev Reverts if an address is already registered under the identifier, it must be removed first. + /// @param name The name of the entry. + /// @param tag The tag grouping the entry. + /// @param newAddress The address to associate with the name and tag. + function setEntry(string calldata name, string calldata tag, address newAddress) external; + + /// @notice Registers the canonical Hub associated with a name. + /// @dev Registering the zero address removes the entry and its identifier from enumeration, it reverts if no address is registered. + /// @dev Reverts if an address is already registered under the identifier, it must be removed first. + /// @param name The name of the Hub. + /// @param hub The address of the Hub. + function setCanonicalHub(string calldata name, address hub) external; + + /// @notice Registers the canonical Spoke associated with a name. + /// @dev Registering the zero address removes the entry and its identifier from enumeration, it reverts if no address is registered. + /// @dev Reverts if an address is already registered under the identifier, it must be removed first. + /// @param name The name of the Spoke. + /// @param spoke The address of the Spoke. + function setCanonicalSpoke(string calldata name, address spoke) external; + + /// @notice Registers the tokenization Spoke associated with a name. + /// @dev Registering the zero address removes the entry and its identifier from enumeration, it reverts if no address is registered. + /// @dev Reverts if an address is already registered under the identifier, it must be removed first. + /// @param name The name of the Spoke. + /// @param spoke The address of the Spoke. + function setTokenizationSpoke(string calldata name, address spoke) external; + + /// @notice Registers the treasury Spoke associated with a name. + /// @dev Registering the zero address removes the entry and its identifier from enumeration, it reverts if no address is registered. + /// @dev Reverts if an address is already registered under the identifier, it must be removed first. + /// @param name The name of the Spoke. + /// @param spoke The address of the Spoke. + function setTreasurySpoke(string calldata name, address spoke) external; + + /// @notice Returns the address associated with an identifier. + /// @param id The identifier of the entry. + /// @return The address of the entry, the zero address if none is registered. + function getAddress(bytes32 id) external view returns (address); + + /// @notice Returns the address associated with a name and tag. + /// @param name The name of the entry. + /// @param tag The tag grouping the entry. + /// @return The address of the entry, the zero address if none is registered. + function getAddress(string calldata name, string calldata tag) external view returns (address); + + /// @notice Returns the entry associated with an identifier. + /// @param id The identifier of the entry. + /// @return The entry associated with the identifier. + function getEntry(bytes32 id) external view returns (Entry memory); + + /// @notice Returns the number of tags with at least one registered entry. + function getTagCount() external view returns (uint256); + + /// @notice Returns all tags with at least one registered entry. + function getTags() external view returns (string[] memory); + + /// @notice Returns a slice of the tags with at least one registered entry. + /// @dev Out-of-range bounds are clamped to the number of tags, it does not revert. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of tags in the slice. + function getTags(uint256 start, uint256 end) external view returns (string[] memory); + + /// @notice Returns the number of entries grouped under a tag. + /// @param tag The tag grouping the entries. + /// @return The number of entries. + function getIdCount(string calldata tag) external view returns (uint256); + + /// @notice Returns the identifiers of all entries grouped under a tag. + /// @param tag The tag grouping the entries. + /// @return The list of identifiers. + function getIds(string calldata tag) external view returns (bytes32[] memory); + + /// @notice Returns a slice of the identifiers of the entries grouped under a tag. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param tag The tag grouping the entries. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of identifiers in the slice. + function getIds( + string calldata tag, + uint256 start, + uint256 end + ) external view returns (bytes32[] memory); + + /// @notice Returns the addresses of all entries grouped under a tag. + /// @param tag The tag grouping the entries. + /// @return The list of addresses. + function getAddresses(string calldata tag) external view returns (address[] memory); + + /// @notice Returns a slice of the addresses of the entries grouped under a tag. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param tag The tag grouping the entries. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of addresses in the slice. + function getAddresses( + string calldata tag, + uint256 start, + uint256 end + ) external view returns (address[] memory); + + /// @notice Returns the number of entries registered for an address. + /// @param addr The registered address. + /// @return The number of entries. + function getAddressIdCount(address addr) external view returns (uint256); + + /// @notice Returns the identifiers of all entries registered for an address. + /// @param addr The registered address. + /// @return The list of identifiers. + function getAddressIds(address addr) external view returns (bytes32[] memory); + + /// @notice Returns a slice of the identifiers of the entries registered for an address. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param addr The registered address. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of identifiers in the slice. + function getAddressIds( + address addr, + uint256 start, + uint256 end + ) external view returns (bytes32[] memory); + + /// @notice Returns all entries registered for an address. + /// @param addr The registered address. + /// @return The list of entries. + function getEntries(address addr) external view returns (Entry[] memory); + + /// @notice Returns a slice of the entries registered for an address. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param addr The registered address. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of entries in the slice. + function getEntries( + address addr, + uint256 start, + uint256 end + ) external view returns (Entry[] memory); + + /// @notice Returns whether an address is registered under a tag. + /// @param addr The registered address. + /// @param tag The tag grouping the entries. + /// @return True if at least one entry associates the address with the tag. + function isRegistered(address addr, string calldata tag) external view returns (bool); + + /// @notice Returns the canonical Hub associated with a name. + /// @param name The name of the Hub. + /// @return The address of the Hub, the zero address if none is registered. + function getCanonicalHub(string calldata name) external view returns (address); + + /// @notice Returns the addresses of all registered canonical Hubs. + /// @return The list of canonical Hub addresses. + function getCanonicalHubs() external view returns (address[] memory); + + /// @notice Returns a slice of the addresses of the registered canonical Hubs. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of canonical Hub addresses in the slice. + function getCanonicalHubs(uint256 start, uint256 end) external view returns (address[] memory); + + /// @notice Returns the canonical Spoke associated with a name. + /// @param name The name of the Spoke. + /// @return The address of the Spoke, the zero address if none is registered. + function getCanonicalSpoke(string calldata name) external view returns (address); + + /// @notice Returns the addresses of all registered canonical Spokes. + /// @return The list of canonical Spoke addresses. + function getCanonicalSpokes() external view returns (address[] memory); + + /// @notice Returns a slice of the addresses of the registered canonical Spokes. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of canonical Spoke addresses in the slice. + function getCanonicalSpokes(uint256 start, uint256 end) external view returns (address[] memory); + + /// @notice Returns the tokenization Spoke associated with a name. + /// @param name The name of the Spoke. + /// @return The address of the Spoke, the zero address if none is registered. + function getTokenizationSpoke(string calldata name) external view returns (address); + + /// @notice Returns the addresses of all registered tokenization Spokes. + /// @return The list of tokenization Spoke addresses. + function getTokenizationSpokes() external view returns (address[] memory); + + /// @notice Returns a slice of the addresses of the registered tokenization Spokes. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of tokenization Spoke addresses in the slice. + function getTokenizationSpokes( + uint256 start, + uint256 end + ) external view returns (address[] memory); + + /// @notice Returns the treasury Spoke associated with a name. + /// @param name The name of the Spoke. + /// @return The address of the Spoke, the zero address if none is registered. + function getTreasurySpoke(string calldata name) external view returns (address); + + /// @notice Returns the addresses of all registered treasury Spokes. + /// @return The list of treasury Spoke addresses. + function getTreasurySpokes() external view returns (address[] memory); + + /// @notice Returns a slice of the addresses of the registered treasury Spokes. + /// @dev Out-of-range bounds are clamped to the number of entries, it does not revert. + /// @param start The start index of the slice. + /// @param end The end index of the slice. + /// @return The list of treasury Spoke addresses in the slice. + function getTreasurySpokes(uint256 start, uint256 end) external view returns (address[] memory); + + /// @notice Returns the tag grouping all canonical Hubs. + function CANONICAL_HUB_TAG() external view returns (string memory); + + /// @notice Returns the tag grouping all canonical Spokes. + function CANONICAL_SPOKE_TAG() external view returns (string memory); + + /// @notice Returns the tag grouping all tokenization Spokes. + function TOKENIZATION_SPOKE_TAG() external view returns (string memory); + + /// @notice Returns the tag grouping all treasury Spokes. + function TREASURY_SPOKE_TAG() external view returns (string memory); + + /// @notice Returns the identifier of the entry associated with a name and tag. + /// @dev The identifier is the hash of the ABI-encoded name and tag. + /// @param name The name of the entry. + /// @param tag The tag grouping the entry. + /// @return The identifier of the entry. + function getId(string calldata name, string calldata tag) external pure returns (bytes32); +} diff --git a/src/config-engine/AaveV4ConfigEngine.sol b/src/config-engine/AaveV4ConfigEngine.sol index 1c8597920..50a860c42 100644 --- a/src/config-engine/AaveV4ConfigEngine.sol +++ b/src/config-engine/AaveV4ConfigEngine.sol @@ -5,92 +5,115 @@ import {HubEngine} from 'src/config-engine/libraries/HubEngine.sol'; import {SpokeEngine} from 'src/config-engine/libraries/SpokeEngine.sol'; import {AccessManagerEngine} from 'src/config-engine/libraries/AccessManagerEngine.sol'; import {PositionManagerEngine} from 'src/config-engine/libraries/PositionManagerEngine.sol'; +import {AddressesProviderEngine} from 'src/config-engine/libraries/AddressesProviderEngine.sol'; import {IAaveV4ConfigEngine} from 'src/config-engine/interfaces/IAaveV4ConfigEngine.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; /// @title AaveV4ConfigEngine /// @author Aave Labs /// @notice Implementation of IAaveV4ConfigEngine. Delegates to external library contracts for /// each action category. Invoked via delegatecall from payload contracts. +/// @dev Hub and Spoke actions revert when the targeted Hub or Spoke is not registered on the +/// AddressesProvider; entries are managed via `executeAddressesProviderEntryUpdates`. contract AaveV4ConfigEngine is IAaveV4ConfigEngine { + /// @inheritdoc IAaveV4ConfigEngine + IAddressesProvider public immutable ADDRESSES_PROVIDER; + + /// @dev Thrown when the addresses provider address is zero. + error InvalidAddressesProvider(); + + /// @param addressesProvider_ The AddressesProvider authorizing and registering engine actions. + constructor(IAddressesProvider addressesProvider_) { + require(address(addressesProvider_) != address(0), InvalidAddressesProvider()); + ADDRESSES_PROVIDER = addressesProvider_; + } + + /// @inheritdoc IAaveV4ConfigEngine + function executeAddressesProviderEntryUpdates( + AddressesProviderEntryUpdate[] calldata updates + ) external { + AddressesProviderEngine.executeAddressesProviderEntryUpdates(updates, ADDRESSES_PROVIDER); + } + /// @inheritdoc IAaveV4ConfigEngine function executeHubAssetListings(AssetListing[] calldata listings) external { - HubEngine.executeHubAssetListings(listings); + HubEngine.executeHubAssetListings(listings, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubAssetConfigUpdates(AssetConfigUpdate[] calldata updates) external { - HubEngine.executeHubAssetConfigUpdates(updates); + HubEngine.executeHubAssetConfigUpdates(updates, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubSpokeToAssetsAdditions(SpokeToAssetsAddition[] calldata additions) external { - HubEngine.executeHubSpokeToAssetsAdditions(additions); + HubEngine.executeHubSpokeToAssetsAdditions(additions, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubSpokeConfigUpdates(SpokeConfigUpdate[] calldata updates) external { - HubEngine.executeHubSpokeConfigUpdates(updates); + HubEngine.executeHubSpokeConfigUpdates(updates, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubAssetHalts(AssetHalt[] calldata halts) external { - HubEngine.executeHubAssetHalts(halts); + HubEngine.executeHubAssetHalts(halts, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubAssetDeactivations(AssetDeactivation[] calldata deactivations) external { - HubEngine.executeHubAssetDeactivations(deactivations); + HubEngine.executeHubAssetDeactivations(deactivations, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubAssetCapsResets(AssetCapsReset[] calldata resets) external { - HubEngine.executeHubAssetCapsResets(resets); + HubEngine.executeHubAssetCapsResets(resets, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubSpokeDeactivations(SpokeDeactivation[] calldata deactivations) external { - HubEngine.executeHubSpokeDeactivations(deactivations); + HubEngine.executeHubSpokeDeactivations(deactivations, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeHubSpokeCapsResets(SpokeCapsReset[] calldata resets) external { - HubEngine.executeHubSpokeCapsResets(resets); + HubEngine.executeHubSpokeCapsResets(resets, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeSpokeReserveListings(ReserveListing[] calldata listings) external { - SpokeEngine.executeSpokeReserveListings(listings); + SpokeEngine.executeSpokeReserveListings(listings, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeSpokeReserveConfigUpdates(ReserveConfigUpdate[] calldata updates) external { - SpokeEngine.executeSpokeReserveConfigUpdates(updates); + SpokeEngine.executeSpokeReserveConfigUpdates(updates, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeSpokeLiquidationConfigUpdates( LiquidationConfigUpdate[] calldata updates ) external { - SpokeEngine.executeSpokeLiquidationConfigUpdates(updates); + SpokeEngine.executeSpokeLiquidationConfigUpdates(updates, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeSpokeDynamicReserveConfigAdditions( DynamicReserveConfigAddition[] calldata additions ) external { - SpokeEngine.executeSpokeDynamicReserveConfigAdditions(additions); + SpokeEngine.executeSpokeDynamicReserveConfigAdditions(additions, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeSpokeDynamicReserveConfigUpdates( DynamicReserveConfigUpdate[] calldata updates ) external { - SpokeEngine.executeSpokeDynamicReserveConfigUpdates(updates); + SpokeEngine.executeSpokeDynamicReserveConfigUpdates(updates, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine function executeSpokePositionManagerUpdates(PositionManagerUpdate[] calldata updates) external { - SpokeEngine.executeSpokePositionManagerUpdates(updates); + SpokeEngine.executeSpokePositionManagerUpdates(updates, ADDRESSES_PROVIDER); } /// @inheritdoc IAaveV4ConfigEngine diff --git a/src/config-engine/AaveV4Payload.sol b/src/config-engine/AaveV4Payload.sol index f54b249af..fd00ca055 100644 --- a/src/config-engine/AaveV4Payload.sol +++ b/src/config-engine/AaveV4Payload.sol @@ -28,12 +28,26 @@ abstract contract AaveV4Payload { function execute() external { _preExecute(); _executeAccessManagerActions(); + _executeAddressesProviderActions(); _executeHubActions(); _executeSpokeActions(); _executePositionManagerActions(); _postExecute(); } + /// @notice Returns the AddressesProvider entry updates to execute. Override to provide updates. + /// @dev Executed before Hub and Spoke actions, so a Hub or Spoke can be registered and targeted + /// within the same payload. + /// @return An array of AddressesProviderEntryUpdate structs (empty by default). + function addressesProviderEntryUpdates() + public + view + virtual + returns (IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] memory) + { + return new IAaveV4ConfigEngine.AddressesProviderEntryUpdate[](0); + } + /// @notice Returns the Hub asset listings to execute. Override to provide listings. /// @return An array of AssetListing structs (empty by default). function hubAssetListings() @@ -260,6 +274,17 @@ abstract contract AaveV4Payload { return new IAaveV4ConfigEngine.PositionManagerRoleRenouncement[](0); } + /// @notice Executes all AddressesProvider entry updates via delegatecall to the engine. + function _executeAddressesProviderActions() internal { + IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] + memory updates = addressesProviderEntryUpdates(); + if (updates.length > 0) { + _delegateCallEngine( + abi.encodeCall(IAaveV4ConfigEngine.executeAddressesProviderEntryUpdates, (updates)) + ); + } + } + /// @notice Executes all hub-related configuration actions via delegatecall to the engine. function _executeHubActions() internal { IAaveV4ConfigEngine.AssetListing[] memory listings = hubAssetListings(); diff --git a/src/config-engine/interfaces/IAaveV4ConfigEngine.sol b/src/config-engine/interfaces/IAaveV4ConfigEngine.sol index 141fbabbb..109710234 100644 --- a/src/config-engine/interfaces/IAaveV4ConfigEngine.sol +++ b/src/config-engine/interfaces/IAaveV4ConfigEngine.sol @@ -6,6 +6,7 @@ import {ISpokeConfigurator} from 'src/spoke/interfaces/ISpokeConfigurator.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; /// @title IAaveV4ConfigEngine /// @author Aave Labs @@ -13,22 +14,37 @@ import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateS /// The engine is stateless and invoked via delegatecall from payload contracts. /// All numeric fields in config structs use uint256 so that type(uint256).max can serve as /// the universal KEEP_CURRENT sentinel. Boolean fields use uint256 (0=false, 1=true, KEEP_CURRENT=skip). +/// Hub and Spoke actions revert when the targeted Hub or Spoke is not registered on the +/// AddressesProvider; entries are managed via `executeAddressesProviderEntryUpdates`. interface IAaveV4ConfigEngine { /// @notice Parameters for tokenization of an asset on a Hub when listing the asset. - /// @dev Tokenization is skipped only when all fields are unset. Otherwise `name`, `symbol` and - /// `proxyAdminOwner` are all required; a partially set config reverts. + /// @dev Tokenization is skipped only when all fields are unset. Otherwise `name`, `symbol`, + /// `proxyAdminOwner` and `registrationName` are all required; a partially set config reverts. /// @dev addCap The add cap for the TokenizationSpoke. /// @dev proxyAdminOwner The owner to set on the ProxyAdmin of the deployed TokenizationSpoke (address(0) when unset). /// @dev name The name for the TokenizationSpoke ('' when unset). /// @dev symbol The symbol for the TokenizationSpoke ('' when unset). + /// @dev registrationName The name to register the deployed TokenizationSpoke under on the AddressesProvider ('' when unset). struct TokenizationSpokeConfig { uint256 addCap; address proxyAdminOwner; string name; string symbol; + string registrationName; + } + + /// @notice Parameters for updating an entry on the AddressesProvider. + /// @dev name The name of the entry. + /// @dev tag The tag grouping the entry. + /// @dev addr The address to register; the zero address removes the entry. + struct AddressesProviderEntryUpdate { + string name; + string tag; + address addr; } /// @notice Parameters for listing a new asset on a Hub. + /// @dev The Hub must be registered on the AddressesProvider as a canonical Hub. /// @dev hubConfigurator The HubConfigurator to use for this action. /// @dev hub The address of the Hub. /// @dev underlying The address of the underlying asset. @@ -163,6 +179,7 @@ interface IAaveV4ConfigEngine { } /// @notice Parameters for listing a new reserve on a Spoke. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @dev spokeConfigurator The SpokeConfigurator to use for this action. /// @dev spoke The address of the Spoke. /// @dev hub The address of the Hub. @@ -342,6 +359,14 @@ interface IAaveV4ConfigEngine { uint32 newDelay; } + /// @notice Updates entries on the AddressesProvider. + /// @dev Hubs and Spokes must be registered on the AddressesProvider before other engine actions + /// can target them; this is the action to register (or unregister) them. + /// @param updates The entry updates to execute. + function executeAddressesProviderEntryUpdates( + AddressesProviderEntryUpdate[] calldata updates + ) external; + /// @notice Lists new assets on Hubs via the HubConfigurator. /// @param listings The asset listings to execute. function executeHubAssetListings(AssetListing[] calldata listings) external; @@ -438,4 +463,7 @@ interface IAaveV4ConfigEngine { /// @notice Updates target admin delays via AccessManager. /// @param updates The target admin delay updates to execute. function executeTargetAdminDelayUpdates(TargetAdminDelayUpdate[] calldata updates) external; + + /// @notice Returns the AddressesProvider used to authorize and register engine actions. + function ADDRESSES_PROVIDER() external view returns (IAddressesProvider); } diff --git a/src/config-engine/libraries/AddressesProviderEngine.sol b/src/config-engine/libraries/AddressesProviderEngine.sol new file mode 100644 index 000000000..5207a1f2c --- /dev/null +++ b/src/config-engine/libraries/AddressesProviderEngine.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; +import {IAaveV4ConfigEngine} from 'src/config-engine/interfaces/IAaveV4ConfigEngine.sol'; + +/// @title AddressesProviderEngine +/// @author Aave Labs +/// @notice Library containing AddressesProvider logic for AaveV4ConfigEngine. +library AddressesProviderEngine { + /// @notice Updates entries on the AddressesProvider. + /// @param updates The entry updates to execute. + /// @param addressesProvider The AddressesProvider to update. + function executeAddressesProviderEntryUpdates( + IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] calldata updates, + IAddressesProvider addressesProvider + ) external { + uint256 length = updates.length; + for (uint256 i; i < length; ++i) { + addressesProvider.setEntry(updates[i].name, updates[i].tag, updates[i].addr); + } + } +} diff --git a/src/config-engine/libraries/EngineUtils.sol b/src/config-engine/libraries/EngineUtils.sol new file mode 100644 index 000000000..4d19ffb6c --- /dev/null +++ b/src/config-engine/libraries/EngineUtils.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; + +/// @title EngineUtils +/// @author Aave Labs +/// @notice Library containing shared helpers for the AaveV4ConfigEngine libraries. +library EngineUtils { + /// @dev Thrown when a Hub targeted by an engine action is not registered on the + /// AddressesProvider as a canonical Hub. + error HubNotRegistered(address hub); + + /// @dev Thrown when a Spoke targeted by an engine action is not registered on the + /// AddressesProvider under a spoke tag. + error SpokeNotRegistered(address spoke); + + /// @dev Thrown when a Spoke targeted by an engine action that only supports canonical Spokes is + /// not registered on the AddressesProvider as a canonical Spoke. + error CanonicalSpokeNotRegistered(address spoke); + + /// @dev Reverts unless the Hub is registered on the AddressesProvider as a canonical Hub. + function requireRegisteredHub(IAddressesProvider addressesProvider, address hub) internal view { + require( + addressesProvider.isRegistered(hub, addressesProvider.CANONICAL_HUB_TAG()), + HubNotRegistered(hub) + ); + } + + /// @dev Reverts unless the Spoke is registered on the AddressesProvider under a spoke tag + /// (canonical, tokenization or treasury). For flows supporting any spoke participant. + function requireRegisteredSpoke( + IAddressesProvider addressesProvider, + address spoke + ) internal view { + require( + addressesProvider.isRegistered(spoke, addressesProvider.CANONICAL_SPOKE_TAG()) || + addressesProvider.isRegistered(spoke, addressesProvider.TOKENIZATION_SPOKE_TAG()) || + addressesProvider.isRegistered(spoke, addressesProvider.TREASURY_SPOKE_TAG()), + SpokeNotRegistered(spoke) + ); + } + + /// @dev Reverts unless the Spoke is registered on the AddressesProvider as a canonical Spoke. + /// For flows that only canonical Spokes support (reserves, liquidations, position managers). + function requireRegisteredCanonicalSpoke( + IAddressesProvider addressesProvider, + address spoke + ) internal view { + require( + addressesProvider.isRegistered(spoke, addressesProvider.CANONICAL_SPOKE_TAG()), + CanonicalSpokeNotRegistered(spoke) + ); + } +} diff --git a/src/config-engine/libraries/HubEngine.sol b/src/config-engine/libraries/HubEngine.sol index 8cc74e8ad..e80bb8e40 100644 --- a/src/config-engine/libraries/HubEngine.sol +++ b/src/config-engine/libraries/HubEngine.sol @@ -3,11 +3,13 @@ pragma solidity ^0.8.0; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {EngineFlags} from 'src/config-engine/libraries/EngineFlags.sol'; +import {EngineUtils} from 'src/config-engine/libraries/EngineUtils.sol'; import {TokenizationSpokeDeployer} from 'src/config-engine/libraries/TokenizationSpokeDeployer.sol'; import {IHubBase} from 'src/hub/interfaces/IHubBase.sol'; import {IHub} from 'src/hub/interfaces/IHub.sol'; import {IAssetInterestRateStrategy} from 'src/hub/interfaces/IAssetInterestRateStrategy.sol'; import {IAaveV4ConfigEngine} from 'src/config-engine/interfaces/IAaveV4ConfigEngine.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; /// @title HubEngine /// @author Aave Labs @@ -20,18 +22,26 @@ library HubEngine { error InvalidIrDataWithNewStrategy(); /// @dev Thrown when a tokenization config is partially set. Either all fields are unset (no - /// TokenizationSpoke) or `name`, `symbol` and `proxyAdminOwner` must all be provided. + /// TokenizationSpoke) or `name`, `symbol`, `proxyAdminOwner` and `registrationName` must all be + /// provided. error InvalidTokenizationSpokeConfig(); /// @notice Lists new assets on Hubs via the HubConfigurator. + /// @dev The Hub must be registered on the AddressesProvider as a canonical Hub. /// @dev When tokenization data is set, also deploys a TokenizationSpoke (impl + proxy) via - /// CREATE2 and registers it on the Hub for the listed asset. + /// CREATE2 and registers it on the AddressesProvider and on the Hub for the listed asset. /// @param listings The asset listings to execute. - function executeHubAssetListings(IAaveV4ConfigEngine.AssetListing[] calldata listings) external { + /// @param addressesProvider The AddressesProvider authorizing and registering the actions. + function executeHubAssetListings( + IAaveV4ConfigEngine.AssetListing[] calldata listings, + IAddressesProvider addressesProvider + ) external { uint256 length = listings.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, listings[i].hub); + bytes memory irData = abi.encode(listings[i].irData); - listings[i].hubConfigurator.addAsset( + uint256 assetId = listings[i].hubConfigurator.addAsset( listings[i].hub, listings[i].underlying, listings[i].feeReceiver, @@ -40,7 +50,7 @@ library HubEngine { irData ); - _deployAndRegisterTokenizationSpoke(listings[i]); + _deployAndRegisterTokenizationSpoke(listings[i], assetId, addressesProvider); } } @@ -50,12 +60,17 @@ library HubEngine { /// IR: strategy set → updateInterestRateStrategy; strategy kept + non-sentinel irData fields → /// read-modify-write via updateInterestRateData. /// Reinvestment: address set → updateReinvestmentController. + /// @dev The Hub must be registered on the AddressesProvider as a canonical Hub. /// @param updates The asset config updates to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubAssetConfigUpdates( - IAaveV4ConfigEngine.AssetConfigUpdate[] calldata updates + IAaveV4ConfigEngine.AssetConfigUpdate[] calldata updates, + IAddressesProvider addressesProvider ) external { uint256 length = updates.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, updates[i].hub); + uint256 assetId = IHubBase(updates[i].hub).getAssetId(updates[i].underlying); bool updateFee = updates[i].liquidityFee != EngineFlags.KEEP_CURRENT; @@ -95,12 +110,18 @@ library HubEngine { } /// @notice Registers Spokes for multiple assets on Hubs. + /// @dev The Hub and the added Spoke must both be registered on the AddressesProvider. /// @param additions The Spoke-to-assets additions to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubSpokeToAssetsAdditions( - IAaveV4ConfigEngine.SpokeToAssetsAddition[] calldata additions + IAaveV4ConfigEngine.SpokeToAssetsAddition[] calldata additions, + IAddressesProvider addressesProvider ) external { uint256 length = additions.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, additions[i].hub); + EngineUtils.requireRegisteredSpoke(addressesProvider, additions[i].spoke); + uint256 assetsLength = additions[i].assets.length; uint256[] memory assetIds = new uint256[](assetsLength); IHub.SpokeConfig[] memory configs = new IHub.SpokeConfig[](assetsLength); @@ -122,12 +143,18 @@ library HubEngine { /// Caps: both set → updateSpokeCaps; only add → updateSpokeAddCap; only draw → updateSpokeDrawCap. /// Risk premium threshold: set → updateSpokeRiskPremiumThreshold. /// Status: active set → updateSpokeActive; halted set → updateSpokeHalted. + /// @dev The Hub and the Spoke must both be registered on the AddressesProvider. /// @param updates The Spoke config updates to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubSpokeConfigUpdates( - IAaveV4ConfigEngine.SpokeConfigUpdate[] calldata updates + IAaveV4ConfigEngine.SpokeConfigUpdate[] calldata updates, + IAddressesProvider addressesProvider ) external { uint256 length = updates.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, updates[i].hub); + EngineUtils.requireRegisteredSpoke(addressesProvider, updates[i].spoke); + uint256 assetId = IHubBase(updates[i].hub).getAssetId(updates[i].underlying); _updateSpokeCaps(assetId, updates[i]); @@ -161,46 +188,69 @@ library HubEngine { } /// @notice Halts assets on Hubs. + /// @dev The Hub must be registered on the AddressesProvider as a canonical Hub. /// @param halts The asset halts to execute. - function executeHubAssetHalts(IAaveV4ConfigEngine.AssetHalt[] calldata halts) external { + /// @param addressesProvider The AddressesProvider authorizing the actions. + function executeHubAssetHalts( + IAaveV4ConfigEngine.AssetHalt[] calldata halts, + IAddressesProvider addressesProvider + ) external { uint256 length = halts.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, halts[i].hub); + uint256 assetId = IHubBase(halts[i].hub).getAssetId(halts[i].underlying); halts[i].hubConfigurator.haltAsset(halts[i].hub, assetId); } } /// @notice Deactivates assets on Hubs. + /// @dev The Hub must be registered on the AddressesProvider as a canonical Hub. /// @param deactivations The asset deactivations to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubAssetDeactivations( - IAaveV4ConfigEngine.AssetDeactivation[] calldata deactivations + IAaveV4ConfigEngine.AssetDeactivation[] calldata deactivations, + IAddressesProvider addressesProvider ) external { uint256 length = deactivations.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, deactivations[i].hub); + uint256 assetId = IHubBase(deactivations[i].hub).getAssetId(deactivations[i].underlying); deactivations[i].hubConfigurator.deactivateAsset(deactivations[i].hub, assetId); } } /// @notice Resets asset caps on Hubs. + /// @dev The Hub must be registered on the AddressesProvider as a canonical Hub. /// @param resets The asset caps resets to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubAssetCapsResets( - IAaveV4ConfigEngine.AssetCapsReset[] calldata resets + IAaveV4ConfigEngine.AssetCapsReset[] calldata resets, + IAddressesProvider addressesProvider ) external { uint256 length = resets.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, resets[i].hub); + uint256 assetId = IHubBase(resets[i].hub).getAssetId(resets[i].underlying); resets[i].hubConfigurator.resetAssetCaps(resets[i].hub, assetId); } } /// @notice Deactivates Spokes on Hubs. + /// @dev The Hub and the Spoke must both be registered on the AddressesProvider. /// @param deactivations The Spoke deactivations to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubSpokeDeactivations( - IAaveV4ConfigEngine.SpokeDeactivation[] calldata deactivations + IAaveV4ConfigEngine.SpokeDeactivation[] calldata deactivations, + IAddressesProvider addressesProvider ) external { uint256 length = deactivations.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, deactivations[i].hub); + EngineUtils.requireRegisteredSpoke(addressesProvider, deactivations[i].spoke); + deactivations[i].hubConfigurator.deactivateSpoke( deactivations[i].hub, deactivations[i].spoke @@ -209,32 +259,50 @@ library HubEngine { } /// @notice Resets Spoke caps on Hubs. + /// @dev The Hub and the Spoke must both be registered on the AddressesProvider. /// @param resets The Spoke caps resets to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeHubSpokeCapsResets( - IAaveV4ConfigEngine.SpokeCapsReset[] calldata resets + IAaveV4ConfigEngine.SpokeCapsReset[] calldata resets, + IAddressesProvider addressesProvider ) external { uint256 length = resets.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredHub(addressesProvider, resets[i].hub); + EngineUtils.requireRegisteredSpoke(addressesProvider, resets[i].spoke); + resets[i].hubConfigurator.resetSpokeCaps(resets[i].hub, resets[i].spoke); } } - /// @dev Deploys a TokenizationSpoke (impl + proxy) via CREATE2 and registers it on the Hub. - /// Skipped only when the tokenization config is fully unset; a partially set config reverts - /// instead of being silently ignored. + /// @dev Deploys a TokenizationSpoke (impl + proxy) via CREATE2 and registers it on the + /// AddressesProvider and on the Hub. Skipped only when the tokenization config is fully unset; + /// a partially set config reverts instead of being silently ignored. function _deployAndRegisterTokenizationSpoke( - IAaveV4ConfigEngine.AssetListing calldata listing + IAaveV4ConfigEngine.AssetListing calldata listing, + uint256 assetId, + IAddressesProvider addressesProvider ) private { IAaveV4ConfigEngine.TokenizationSpokeConfig calldata tokenization = listing.tokenization; bool hasName = bytes(tokenization.name).length > 0; bool hasSymbol = bytes(tokenization.symbol).length > 0; bool hasProxyAdminOwner = tokenization.proxyAdminOwner != address(0); - - if (!hasName && !hasSymbol && !hasProxyAdminOwner && tokenization.addCap == 0) { + bool hasRegistrationName = bytes(tokenization.registrationName).length > 0; + + if ( + !hasName && + !hasSymbol && + !hasProxyAdminOwner && + !hasRegistrationName && + tokenization.addCap == 0 + ) { return; } - require(hasName && hasSymbol && hasProxyAdminOwner, InvalidTokenizationSpokeConfig()); + require( + hasName && hasSymbol && hasProxyAdminOwner && hasRegistrationName, + InvalidTokenizationSpokeConfig() + ); address proxy = TokenizationSpokeDeployer.deploy({ hub: listing.hub, @@ -244,7 +312,7 @@ library HubEngine { proxyAdminOwner: tokenization.proxyAdminOwner }); - uint256 assetId = IHubBase(listing.hub).getAssetId(listing.underlying); + addressesProvider.setTokenizationSpoke(tokenization.registrationName, proxy); listing.hubConfigurator.addSpoke( listing.hub, diff --git a/src/config-engine/libraries/SpokeEngine.sol b/src/config-engine/libraries/SpokeEngine.sol index 03f2620dd..e49f00154 100644 --- a/src/config-engine/libraries/SpokeEngine.sol +++ b/src/config-engine/libraries/SpokeEngine.sol @@ -3,9 +3,11 @@ pragma solidity ^0.8.0; import {SafeCast} from 'src/dependencies/openzeppelin/SafeCast.sol'; import {EngineFlags} from 'src/config-engine/libraries/EngineFlags.sol'; +import {EngineUtils} from 'src/config-engine/libraries/EngineUtils.sol'; import {IHubBase} from 'src/hub/interfaces/IHubBase.sol'; import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; import {IAaveV4ConfigEngine} from 'src/config-engine/interfaces/IAaveV4ConfigEngine.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; /// @title SpokeEngine /// @author Aave Labs @@ -14,12 +16,17 @@ library SpokeEngine { using SafeCast for uint256; /// @notice Lists new reserves on Spokes. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @param listings The reserve listings to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeSpokeReserveListings( - IAaveV4ConfigEngine.ReserveListing[] calldata listings + IAaveV4ConfigEngine.ReserveListing[] calldata listings, + IAddressesProvider addressesProvider ) external { uint256 length = listings.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, listings[i].spoke); + uint256 assetId = IHubBase(listings[i].hub).getAssetId(listings[i].underlying); listings[i].spokeConfigurator.addReserve( listings[i].spoke, @@ -33,12 +40,17 @@ library SpokeEngine { } /// @notice Updates reserve config on Spokes. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @param updates The reserve config updates to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeSpokeReserveConfigUpdates( - IAaveV4ConfigEngine.ReserveConfigUpdate[] calldata updates + IAaveV4ConfigEngine.ReserveConfigUpdate[] calldata updates, + IAddressesProvider addressesProvider ) external { uint256 length = updates.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, updates[i].spoke); + uint256 reserveId = _resolveReserveId( updates[i].spoke, updates[i].hub, @@ -94,12 +106,17 @@ library SpokeEngine { /// @dev If all three fields (targetHealthFactor, healthFactorForMaxBonus, liquidationBonusFactor) /// are set, calls updateLiquidationConfig with the full struct. Otherwise, each non-KEEP_CURRENT /// field is updated individually via its dedicated setter. If no field is set, the update is skipped. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @param updates The liquidation config updates to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeSpokeLiquidationConfigUpdates( - IAaveV4ConfigEngine.LiquidationConfigUpdate[] calldata updates + IAaveV4ConfigEngine.LiquidationConfigUpdate[] calldata updates, + IAddressesProvider addressesProvider ) external { uint256 length = updates.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, updates[i].spoke); + bool updateTarget = updates[i].targetHealthFactor != EngineFlags.KEEP_CURRENT; bool updateMaxBonus = updates[i].healthFactorForMaxBonus != EngineFlags.KEEP_CURRENT; bool updateBonusFactor = updates[i].liquidationBonusFactor != EngineFlags.KEEP_CURRENT; @@ -137,12 +154,17 @@ library SpokeEngine { } /// @notice Adds dynamic reserve configs on Spokes. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @param additions The dynamic reserve config additions to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeSpokeDynamicReserveConfigAdditions( - IAaveV4ConfigEngine.DynamicReserveConfigAddition[] calldata additions + IAaveV4ConfigEngine.DynamicReserveConfigAddition[] calldata additions, + IAddressesProvider addressesProvider ) external { uint256 length = additions.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, additions[i].spoke); + uint256 reserveId = _resolveReserveId( additions[i].spoke, additions[i].hub, @@ -159,12 +181,17 @@ library SpokeEngine { /// @notice Updates dynamic reserve configs on Spokes. /// @dev Reads the current config, applies only the fields that differ from KEEP_CURRENT, /// and writes back. If no field is modified the external call is skipped entirely. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @param updates The dynamic reserve config updates to execute. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeSpokeDynamicReserveConfigUpdates( - IAaveV4ConfigEngine.DynamicReserveConfigUpdate[] calldata updates + IAaveV4ConfigEngine.DynamicReserveConfigUpdate[] calldata updates, + IAddressesProvider addressesProvider ) external { uint256 length = updates.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, updates[i].spoke); + uint256 reserveId = _resolveReserveId( updates[i].spoke, updates[i].hub, @@ -202,12 +229,17 @@ library SpokeEngine { } /// @notice Updates position managers on Spokes. + /// @dev The Spoke must be registered on the AddressesProvider as a canonical Spoke. /// @param updates The position manager updates to execute on Spokes. + /// @param addressesProvider The AddressesProvider authorizing the actions. function executeSpokePositionManagerUpdates( - IAaveV4ConfigEngine.PositionManagerUpdate[] calldata updates + IAaveV4ConfigEngine.PositionManagerUpdate[] calldata updates, + IAddressesProvider addressesProvider ) external { uint256 length = updates.length; for (uint256 i; i < length; ++i) { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, updates[i].spoke); + updates[i].spokeConfigurator.updatePositionManager( updates[i].spoke, updates[i].positionManager, diff --git a/src/deployments/batches/AddressesProviderBatch.sol b/src/deployments/batches/AddressesProviderBatch.sol new file mode 100644 index 000000000..98fe54f8e --- /dev/null +++ b/src/deployments/batches/AddressesProviderBatch.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {AddressesProviderDeployProcedure} from 'src/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.sol'; + +/// @title AddressesProviderBatch +/// @author Aave Labs +/// @notice Deploys the AddressesProvider contract, producing a batch report. +contract AddressesProviderBatch is AddressesProviderDeployProcedure { + BatchReports.AddressesProviderBatchReport internal _report; + + /// @dev Constructor. + /// @param owner_ The owner of the AddressesProvider proxy admin and initializer. + /// @param salt_ The CREATE2 salt for deterministic deployment. + constructor(address owner_, bytes32 salt_) { + ( + address addressesProviderProxy, + address addressesProviderImplementation + ) = _deployAddressesProvider({owner: owner_, salt: salt_}); + _report = BatchReports.AddressesProviderBatchReport({ + addressesProviderProxy: addressesProviderProxy, + addressesProviderImplementation: addressesProviderImplementation + }); + } + + /// @notice Returns the batch deployment report. + function getReport() external view returns (BatchReports.AddressesProviderBatchReport memory) { + return _report; + } +} diff --git a/src/deployments/libraries/BatchReports.sol b/src/deployments/libraries/BatchReports.sol index b05070568..84705e79f 100644 --- a/src/deployments/libraries/BatchReports.sol +++ b/src/deployments/libraries/BatchReports.sol @@ -40,6 +40,13 @@ library BatchReports { address treasurySpoke; } + /// @dev addressesProviderProxy The deployed AddressesProvider proxy contract address. + /// @dev addressesProviderImplementation The deployed AddressesProvider implementation contract address. + struct AddressesProviderBatchReport { + address addressesProviderProxy; + address addressesProviderImplementation; + } + /// @dev signatureGateway The deployed SignatureGateway contract address. /// @dev nativeGateway The deployed NativeTokenGateway contract address. struct GatewaysBatchReport { diff --git a/src/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.sol b/src/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.sol new file mode 100644 index 000000000..89d2c02c6 --- /dev/null +++ b/src/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.sol @@ -0,0 +1,34 @@ +// 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 {AddressesProviderInstance} from 'src/addresses-provider/instances/AddressesProviderInstance.sol'; + +/// @title AddressesProviderDeployProcedure +/// @author Aave Labs +/// @notice Deploys the AddressesProvider contract behind a transparent proxy. +contract AddressesProviderDeployProcedure is AaveV4DeployProcedureBase { + /// @notice Deploys a AddressesProvider instance via CREATE2 and sets up a transparent proxy. + /// @param owner The owner of the proxy admin and the AddressesProvider initializer. + /// @param salt The CREATE2 salt for deterministic deployment. + /// @return addressesProviderProxy The address of the deployed transparent proxy. + /// @return addressesProviderImplementation The address of the deployed AddressesProvider implementation contract. + function _deployAddressesProvider( + address owner, + bytes32 salt + ) internal returns (address addressesProviderProxy, address addressesProviderImplementation) { + require(owner != address(0), 'invalid owner'); + addressesProviderImplementation = Create2Utils.create2Deploy({ + salt: salt, + bytecode: type(AddressesProviderInstance).creationCode + }); + addressesProviderProxy = Create2Utils.proxify({ + salt: salt, + logic: addressesProviderImplementation, + initialOwner: owner, + data: abi.encodeCall(AddressesProviderInstance.initialize, (owner)) + }); + return (addressesProviderProxy, addressesProviderImplementation); + } +} diff --git a/tests/config-engine/AaveV4Payload.EmptyReturns.t.sol b/tests/config-engine/AaveV4Payload.EmptyReturns.t.sol index 9165aac08..90ab3549f 100644 --- a/tests/config-engine/AaveV4Payload.EmptyReturns.t.sol +++ b/tests/config-engine/AaveV4Payload.EmptyReturns.t.sol @@ -40,6 +40,10 @@ contract AaveV4PayloadEmptyReturnsTest is BaseConfigEngineTest { minimal.execute(); } + function test_addressesProviderEntryUpdates_returnsEmpty() public view { + assertEq(minimal.addressesProviderEntryUpdates().length, 0); + } + function test_hubAssetListings_returnsEmpty() public view { assertEq(minimal.hubAssetListings().length, 0); } diff --git a/tests/config-engine/AaveV4Payload.t.sol b/tests/config-engine/AaveV4Payload.t.sol index fa71d3ac2..75e01be86 100644 --- a/tests/config-engine/AaveV4Payload.t.sol +++ b/tests/config-engine/AaveV4Payload.t.sol @@ -385,6 +385,7 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { function test_execute_hubSpokeToAssetsAdditions() public { (ISpoke newSpoke, ) = _deployNewSpoke(); + _registerSpokeOnProvider(newSpoke); IAaveV4ConfigEngine.SpokeToAssetsAddition[] memory additions = new IAaveV4ConfigEngine.SpokeToAssetsAddition[](1); @@ -642,6 +643,40 @@ contract AaveV4PayloadTest is BaseConfigEngineTest { assertEq(spokeConfig.drawCap, 0); } + function test_execute_addressesProviderEntryUpdates_runBeforeHubAndSpokeActions() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + _seedAsset(hub1(), irStrategy1(), address(newToken), 18); + + // provider writes run in the payload's context here, so it must own the provider + vm.prank(address(engine)); + AddressesProviderInstance(address(addressesProvider)).transferOwnership(address(payload)); + vm.prank(address(payload)); + AddressesProviderInstance(address(addressesProvider)).acceptOwnership(); + + // the payload registers the new Spoke and lists a reserve on it in the same execution; + // the listing only succeeds if the entry updates are executed first + payload.setAddressesProviderEntryUpdates( + _toAddressesProviderEntryUpdateArray( + IAaveV4ConfigEngine.AddressesProviderEntryUpdate({ + name: 'NEW', + tag: addressesProvider.CANONICAL_SPOKE_TAG(), + addr: address(newSpoke) + }) + ) + ); + + IAaveV4ConfigEngine.ReserveListing memory listing = _defaultReserveListing(); + listing.spoke = address(newSpoke); + listing.underlying = address(newToken); + listing.priceSource = _deployMockPriceFeed(newSpoke, address(priceFeedNew)); + payload.setSpokeReserveListings(_toReserveListingArray(listing)); + + payload.execute(); + + assertEq(addressesProvider.getCanonicalSpoke('NEW'), address(newSpoke)); + assertEq(newSpoke.getReserveCount(), 1); + } + function test_execute_spokeReserveListings() public { uint256 newAssetId = _seedAsset(hub1(), irStrategy1(), address(newToken), 18); _seedSpokeOnAsset(hub1(), newAssetId, spoke1()); diff --git a/tests/config-engine/AddressesProviderRegistration.t.sol b/tests/config-engine/AddressesProviderRegistration.t.sol new file mode 100644 index 000000000..affd341ef --- /dev/null +++ b/tests/config-engine/AddressesProviderRegistration.t.sol @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/config-engine/BaseConfigEngine.t.sol'; + +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {EngineUtils} from 'src/config-engine/libraries/EngineUtils.sol'; + +/// @notice Tests the AddressesProvider integration of the config engine: entry updates via the +/// dedicated action, and the requirement that Hubs and Spokes targeted by engine actions are +/// registered on the AddressesProvider. +contract AddressesProviderRegistrationTest is BaseConfigEngineTest { + function _entryUpdate( + string memory name, + string memory tag, + address addr + ) internal pure returns (IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] memory) { + return + _toAddressesProviderEntryUpdateArray( + IAaveV4ConfigEngine.AddressesProviderEntryUpdate({name: name, tag: tag, addr: addr}) + ); + } + + function _unregisterHub1() internal { + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('HUB_1', addressesProvider.CANONICAL_HUB_TAG(), address(0)) + ); + } + + function _unregisterSpoke1() internal { + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('SPOKE_1', addressesProvider.CANONICAL_SPOKE_TAG(), address(0)) + ); + } + + // Entry updates + + function test_executeAddressesProviderEntryUpdates_registers() public { + address configEngine = makeAddr('CONFIG_ENGINE'); + + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('CONFIG_ENGINE', 'PERIPHERY', configEngine) + ); + + assertEq(addressesProvider.getAddress({name: 'CONFIG_ENGINE', tag: 'PERIPHERY'}), configEngine); + assertTrue(addressesProvider.isRegistered(configEngine, 'PERIPHERY')); + } + + function test_executeAddressesProviderEntryUpdates_unregisters() public { + assertTrue( + addressesProvider.isRegistered(address(hub1()), addressesProvider.CANONICAL_HUB_TAG()) + ); + + _unregisterHub1(); + + assertFalse( + addressesProvider.isRegistered(address(hub1()), addressesProvider.CANONICAL_HUB_TAG()) + ); + } + + function test_executeAddressesProviderEntryUpdates_multiple() public { + IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] + memory updates = new IAaveV4ConfigEngine.AddressesProviderEntryUpdate[](2); + updates[0] = IAaveV4ConfigEngine.AddressesProviderEntryUpdate({ + name: 'HUB_1', + tag: addressesProvider.CANONICAL_HUB_TAG(), + addr: address(0) + }); + updates[1] = IAaveV4ConfigEngine.AddressesProviderEntryUpdate({ + name: 'CORE', + tag: addressesProvider.CANONICAL_HUB_TAG(), + addr: address(hub1()) + }); + + engine.executeAddressesProviderEntryUpdates(updates); + + assertEq(addressesProvider.getCanonicalHub('HUB_1'), address(0)); + assertEq(addressesProvider.getCanonicalHub('CORE'), address(hub1())); + } + + function test_executeAddressesProviderEntryUpdates_revertsWith_AddressAlreadySet() public { + string memory tag = addressesProvider.CANONICAL_HUB_TAG(); + bytes32 id = addressesProvider.getId('HUB_1', tag); + IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] memory updates = _entryUpdate( + 'HUB_1', + tag, + address(hub2()) + ); + + vm.expectRevert(abi.encodeWithSelector(IAddressesProvider.AddressAlreadySet.selector, id)); + engine.executeAddressesProviderEntryUpdates(updates); + } + + function test_executeAddressesProviderEntryUpdates_revertsWith_OwnableUnauthorizedAccount() + public + { + vm.prank(address(engine)); + AddressesProviderInstance(address(addressesProvider)).transferOwnership(ADMIN); + vm.prank(ADMIN); + AddressesProviderInstance(address(addressesProvider)).acceptOwnership(); + + vm.expectRevert( + abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(engine)) + ); + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('CONFIG_ENGINE', 'PERIPHERY', makeAddr('CONFIG_ENGINE')) + ); + } + + // Hub actions require a registered Hub + + function test_executeHubAssetListings_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubAssetListings(_toAssetListingArray(_defaultAssetListing())); + } + + function test_executeHubAssetConfigUpdates_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubAssetConfigUpdates(_toAssetConfigUpdateArray(_defaultAssetConfigUpdate())); + } + + function test_executeHubSpokeToAssetsAdditions_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + IAaveV4ConfigEngine.SpokeToAssetsAddition memory addition = IAaveV4ConfigEngine + .SpokeToAssetsAddition({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(spoke1()), + assets: new IAaveV4ConfigEngine.SpokeAssetConfig[](0) + }); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubSpokeToAssetsAdditions(_toSpokeToAssetsAdditionArray(addition)); + } + + function test_executeHubSpokeConfigUpdates_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubSpokeConfigUpdates(_toSpokeConfigUpdateArray(_defaultSpokeConfigUpdate())); + } + + function test_executeHubAssetHalts_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubAssetHalts( + _toAssetHaltArray( + IAaveV4ConfigEngine.AssetHalt({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + underlying: address(weth) + }) + ) + ); + } + + function test_executeHubAssetDeactivations_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubAssetDeactivations( + _toAssetDeactivationArray( + IAaveV4ConfigEngine.AssetDeactivation({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + underlying: address(weth) + }) + ) + ); + } + + function test_executeHubAssetCapsResets_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubAssetCapsResets( + _toAssetCapsResetArray( + IAaveV4ConfigEngine.AssetCapsReset({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + underlying: address(weth) + }) + ) + ); + } + + function test_executeHubSpokeDeactivations_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubSpokeDeactivations( + _toSpokeDeactivationArray( + IAaveV4ConfigEngine.SpokeDeactivation({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(spoke1()) + }) + ) + ); + } + + function test_executeHubSpokeCapsResets_revertsWith_HubNotRegistered() public { + _unregisterHub1(); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, address(hub1()))); + engine.executeHubSpokeCapsResets( + _toSpokeCapsResetArray( + IAaveV4ConfigEngine.SpokeCapsReset({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(spoke1()) + }) + ) + ); + } + + // Spoke actions require a registered Spoke + + function test_executeSpokeReserveListings_revertsWith_CanonicalSpokeNotRegistered() public { + _unregisterSpoke1(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(spoke1())) + ); + engine.executeSpokeReserveListings(_toReserveListingArray(_defaultReserveListing())); + } + + function test_executeSpokeReserveConfigUpdates_revertsWith_CanonicalSpokeNotRegistered() public { + _unregisterSpoke1(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(spoke1())) + ); + engine.executeSpokeReserveConfigUpdates( + _toReserveConfigUpdateArray(_defaultReserveConfigUpdate()) + ); + } + + function test_executeSpokeLiquidationConfigUpdates_revertsWith_CanonicalSpokeNotRegistered() + public + { + _unregisterSpoke1(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(spoke1())) + ); + engine.executeSpokeLiquidationConfigUpdates( + _toLiquidationConfigUpdateArray(_defaultLiquidationConfigUpdate()) + ); + } + + function test_executeSpokeDynamicReserveConfigAdditions_revertsWith_CanonicalSpokeNotRegistered() + public + { + _unregisterSpoke1(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(spoke1())) + ); + engine.executeSpokeDynamicReserveConfigAdditions( + _toDynamicReserveConfigAdditionArray(_defaultDynamicReserveConfigAddition()) + ); + } + + function test_executeSpokeDynamicReserveConfigUpdates_revertsWith_CanonicalSpokeNotRegistered() + public + { + _unregisterSpoke1(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(spoke1())) + ); + engine.executeSpokeDynamicReserveConfigUpdates( + _toDynamicReserveConfigUpdateArray(_defaultDynamicReserveConfigUpdate()) + ); + } + + function test_executeSpokePositionManagerUpdates_revertsWith_CanonicalSpokeNotRegistered() + public + { + _unregisterSpoke1(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(spoke1())) + ); + engine.executeSpokePositionManagerUpdates( + _toPositionManagerUpdateArray(_defaultPositionManagerUpdate()) + ); + } + + // Spokes attached to a Hub asset require registration + + function test_executeHubSpokeToAssetsAdditions_revertsWith_SpokeNotRegistered() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + + IAaveV4ConfigEngine.SpokeToAssetsAddition memory addition = IAaveV4ConfigEngine + .SpokeToAssetsAddition({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(newSpoke), + assets: new IAaveV4ConfigEngine.SpokeAssetConfig[](0) + }); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.SpokeNotRegistered.selector, address(newSpoke)) + ); + engine.executeHubSpokeToAssetsAdditions(_toSpokeToAssetsAdditionArray(addition)); + } + + // Spoke actions require the canonical Spoke tag specifically + + function test_spokeActions_revertWith_CanonicalSpokeNotRegistered_tokenizationTag() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('NEW', addressesProvider.TOKENIZATION_SPOKE_TAG(), address(newSpoke)) + ); + + IAaveV4ConfigEngine.PositionManagerUpdate memory update = _defaultPositionManagerUpdate(); + update.spoke = address(newSpoke); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(newSpoke)) + ); + engine.executeSpokePositionManagerUpdates(_toPositionManagerUpdateArray(update)); + } + + function test_spokeActions_revertWith_CanonicalSpokeNotRegistered_treasuryTag() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('NEW', addressesProvider.TREASURY_SPOKE_TAG(), address(newSpoke)) + ); + + IAaveV4ConfigEngine.PositionManagerUpdate memory update = _defaultPositionManagerUpdate(); + update.spoke = address(newSpoke); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, address(newSpoke)) + ); + engine.executeSpokePositionManagerUpdates(_toPositionManagerUpdateArray(update)); + } + + // Hub-side Spoke references accept any spoke tag + + function test_executeHubSpokeConfigUpdates_revertsWith_SpokeNotRegistered() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + + IAaveV4ConfigEngine.SpokeConfigUpdate memory update = _defaultSpokeConfigUpdate(); + update.spoke = address(newSpoke); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.SpokeNotRegistered.selector, address(newSpoke)) + ); + engine.executeHubSpokeConfigUpdates(_toSpokeConfigUpdateArray(update)); + } + + function test_executeHubSpokeDeactivations_revertsWith_SpokeNotRegistered() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.SpokeNotRegistered.selector, address(newSpoke)) + ); + engine.executeHubSpokeDeactivations( + _toSpokeDeactivationArray( + IAaveV4ConfigEngine.SpokeDeactivation({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(newSpoke) + }) + ) + ); + } + + function test_executeHubSpokeCapsResets_revertsWith_SpokeNotRegistered() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.SpokeNotRegistered.selector, address(newSpoke)) + ); + engine.executeHubSpokeCapsResets( + _toSpokeCapsResetArray( + IAaveV4ConfigEngine.SpokeCapsReset({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(newSpoke) + }) + ) + ); + } + + function test_hubSpokeActions_allowAnySpokeTag() public { + _seedAsset(hub1(), irStrategy1(), address(weth), 18); + (ISpoke newSpoke, ) = _deployNewSpoke(); + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('NEW', addressesProvider.TOKENIZATION_SPOKE_TAG(), address(newSpoke)) + ); + + IAaveV4ConfigEngine.SpokeAssetConfig[] + memory assets = new IAaveV4ConfigEngine.SpokeAssetConfig[](1); + assets[0] = IAaveV4ConfigEngine.SpokeAssetConfig({ + underlying: address(weth), + config: IHub.SpokeConfig({ + addCap: 1000, + drawCap: 500, + riskPremiumThreshold: 100, + active: true, + halted: false + }) + }); + engine.executeHubSpokeToAssetsAdditions( + _toSpokeToAssetsAdditionArray( + IAaveV4ConfigEngine.SpokeToAssetsAddition({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(newSpoke), + assets: assets + }) + ) + ); + + engine.executeHubSpokeDeactivations( + _toSpokeDeactivationArray( + IAaveV4ConfigEngine.SpokeDeactivation({ + hubConfigurator: hubConfigurator, + hub: address(hub1()), + spoke: address(newSpoke) + }) + ) + ); + + assertFalse(hub1().getSpokeConfig(0, address(newSpoke)).active); + } + + // Register-then-act within the same flow + + function test_registerThenList() public { + (ISpoke newSpoke, ) = _deployNewSpoke(); + _seedAsset(hub1(), irStrategy1(), address(weth), 18); + + engine.executeAddressesProviderEntryUpdates( + _entryUpdate('NEW', addressesProvider.CANONICAL_SPOKE_TAG(), address(newSpoke)) + ); + + IAaveV4ConfigEngine.ReserveListing memory listing = _defaultReserveListing(); + listing.spoke = address(newSpoke); + listing.priceSource = _deployMockPriceFeed(newSpoke, tokenList[TOKEN_WETH].priceFeed); + + engine.executeSpokeReserveListings(_toReserveListingArray(listing)); + + assertEq(newSpoke.getReserveId(address(hub1()), 0), 0); + } + + // Tokenization spoke auto-registration + + function test_executeHubAssetListings_registersTokenizationSpoke() public { + IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); + listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + addCap: 1_000, + proxyAdminOwner: PROXY_ADMIN_OWNER, + name: 'Aave WETH', + symbol: 'aWETH', + registrationName: 'HUB1_WETH' + }); + + address expectedProxy = TokenizationSpokeDeployer.computeProxyAddress( + address(hub1()), + address(weth), + 'Aave WETH', + 'aWETH', + PROXY_ADMIN_OWNER + ); + + engine.executeHubAssetListings(_toAssetListingArray(listing)); + + assertEq(addressesProvider.getTokenizationSpoke('HUB1_WETH'), expectedProxy); + assertTrue( + addressesProvider.isRegistered(expectedProxy, addressesProvider.TOKENIZATION_SPOKE_TAG()) + ); + } + + function test_executeHubAssetListings_revertsWith_InvalidTokenizationSpokeConfig_whenNoRegistrationName() + public + { + IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); + listing.tokenization = IAaveV4ConfigEngine.TokenizationSpokeConfig({ + addCap: 1_000, + proxyAdminOwner: PROXY_ADMIN_OWNER, + name: 'Aave WETH', + symbol: 'aWETH', + registrationName: '' + }); + + vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); + engine.executeHubAssetListings(_toAssetListingArray(listing)); + } + + function test_executeHubAssetListings_revertsWith_InvalidTokenizationSpokeConfig_whenOnlyRegistrationName() + public + { + IAaveV4ConfigEngine.AssetListing memory listing = _defaultAssetListing(); + listing.tokenization.registrationName = 'HUB1_WETH'; + + vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); + engine.executeHubAssetListings(_toAssetListingArray(listing)); + } +} diff --git a/tests/config-engine/BaseConfigEngine.t.sol b/tests/config-engine/BaseConfigEngine.t.sol index 89b22cca1..30989c310 100644 --- a/tests/config-engine/BaseConfigEngine.t.sol +++ b/tests/config-engine/BaseConfigEngine.t.sol @@ -28,6 +28,9 @@ import {Create2TestHelper} from 'tests/utils/Create2TestHelper.sol'; import {AaveV4Payload} from 'src/config-engine/AaveV4Payload.sol'; import {AaveV4ConfigEngine} from 'src/config-engine/AaveV4ConfigEngine.sol'; import {IAaveV4ConfigEngine} from 'src/config-engine/interfaces/IAaveV4ConfigEngine.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; +import {AddressesProviderInstance} from 'src/addresses-provider/instances/AddressesProviderInstance.sol'; +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; import {EngineFlags} from 'src/config-engine/libraries/EngineFlags.sol'; import {AccessManagerEngine} from 'src/config-engine/libraries/AccessManagerEngine.sol'; import {HubEngine} from 'src/config-engine/libraries/HubEngine.sol'; @@ -79,6 +82,7 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { address internal PROXY_ADMIN_OWNER = makeAddr('PROXY_ADMIN_OWNER'); MockGovernanceExecutor internal executor; + IAddressesProvider internal addressesProvider; AaveV4ConfigEngine internal engine; IAccessManager internal accessManager; IHubConfigurator internal hubConfigurator; @@ -157,10 +161,12 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { } executor = new MockGovernanceExecutor(PAYLOADS_CONTROLLER); - engine = new AaveV4ConfigEngine(); + addressesProvider = _deployAddressesProvider(address(this)); + engine = new AaveV4ConfigEngine(addressesProvider); positionManager = new PositionManagerBaseWrapper(address(engine)); _setupRoles(report); + _registerTestEnv(); vm.label(address(hubs[0]), 'hub1'); vm.label(address(hubs[1]), 'hub2'); @@ -195,6 +201,48 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { assertEq(vm.getRecordedLogs().length, expectedCount); } + function _deployAddressesProvider(address owner) internal returns (IAddressesProvider) { + return + IAddressesProvider( + address( + new TransparentUpgradeableProxy( + address(new AddressesProviderInstance()), + ADMIN, + abi.encodeCall(AddressesProviderInstance.initialize, (owner)) + ) + ) + ); + } + + /// @dev Registers the test Hubs and Spokes on the AddressesProvider, then hands ownership to the + /// engine, which is the actor making the provider calls when tests invoke it directly. + function _registerTestEnv() internal { + for (uint256 i; i < NUM_HUBS; ++i) { + addressesProvider.setCanonicalHub( + string.concat('HUB_', vm.toString(i + 1)), + address(hubs[i]) + ); + } + for (uint256 i; i < NUM_SPOKES; ++i) { + addressesProvider.setCanonicalSpoke( + string.concat('SPOKE_', vm.toString(i + 1)), + address(spokes[i]) + ); + } + AddressesProviderInstance(address(addressesProvider)).transferOwnership(address(engine)); + vm.prank(address(engine)); + AddressesProviderInstance(address(addressesProvider)).acceptOwnership(); + } + + /// @dev Registers a Spoke on the AddressesProvider as the engine, the provider owner after setUp. + function _registerSpokeOnProvider(ISpoke spoke) internal { + vm.prank(address(engine)); + addressesProvider.setCanonicalSpoke( + string.concat('SPOKE_', vm.toString(address(spoke))), + address(spoke) + ); + } + function _deployNewSpoke() internal returns (ISpoke, IAaveOracle) { vm.startPrank(ADMIN); TestTypes.TestSpokeReport memory report = AaveV4TestOrchestration.deployTestSpoke( @@ -341,7 +389,8 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { addCap: 0, proxyAdminOwner: address(0), name: '', - symbol: '' + symbol: '', + registrationName: '' }) }); } @@ -520,6 +569,13 @@ abstract contract BaseConfigEngineTest is Test, Create2TestHelper { assertEq(actual.halted, expected.halted); } + function _toAddressesProviderEntryUpdateArray( + IAaveV4ConfigEngine.AddressesProviderEntryUpdate memory item + ) internal pure returns (IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] memory arr) { + arr = new IAaveV4ConfigEngine.AddressesProviderEntryUpdate[](1); + arr[0] = item; + } + function _toAssetConfigUpdateArray( IAaveV4ConfigEngine.AssetConfigUpdate memory item ) internal pure returns (IAaveV4ConfigEngine.AssetConfigUpdate[] memory arr) { diff --git a/tests/config-engine/EngineUtils.t.sol b/tests/config-engine/EngineUtils.t.sol new file mode 100644 index 000000000..06fec3dce --- /dev/null +++ b/tests/config-engine/EngineUtils.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {AddressesProviderInstance} from 'src/addresses-provider/instances/AddressesProviderInstance.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; +import {EngineUtils} from 'src/config-engine/libraries/EngineUtils.sol'; + +/// @dev Wrapper to call EngineUtils library functions externally. +contract EngineUtilsHarness { + function requireRegisteredHub(IAddressesProvider addressesProvider, address hub) external view { + EngineUtils.requireRegisteredHub(addressesProvider, hub); + } + + function requireRegisteredSpoke( + IAddressesProvider addressesProvider, + address spoke + ) external view { + EngineUtils.requireRegisteredSpoke(addressesProvider, spoke); + } + + function requireRegisteredCanonicalSpoke( + IAddressesProvider addressesProvider, + address spoke + ) external view { + EngineUtils.requireRegisteredCanonicalSpoke(addressesProvider, spoke); + } +} + +contract EngineUtilsTest is Test { + address internal HUB = makeAddr('HUB'); + address internal SPOKE = makeAddr('SPOKE'); + + EngineUtilsHarness internal _harness; + IAddressesProvider internal _provider; + + function setUp() public { + _harness = new EngineUtilsHarness(); + _provider = IAddressesProvider( + address( + new TransparentUpgradeableProxy( + address(new AddressesProviderInstance()), + makeAddr('PROXY_ADMIN_OWNER'), + abi.encodeCall(AddressesProviderInstance.initialize, (address(this))) + ) + ) + ); + } + + function test_requireRegisteredHub() public { + _provider.setCanonicalHub('CORE', HUB); + _harness.requireRegisteredHub(_provider, HUB); + } + + function test_requireRegisteredHub_revertsWith_HubNotRegistered() public { + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, HUB)); + _harness.requireRegisteredHub(_provider, HUB); + } + + function test_requireRegisteredHub_revertsWith_HubNotRegistered_otherTag() public { + _provider.setEntry({name: 'CORE', tag: 'PERIPHERY', newAddress: HUB}); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, HUB)); + _harness.requireRegisteredHub(_provider, HUB); + } + + function test_requireRegisteredHub_revertsWith_HubNotRegistered_spokeTag() public { + _provider.setCanonicalSpoke('CORE', HUB); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.HubNotRegistered.selector, HUB)); + _harness.requireRegisteredHub(_provider, HUB); + } + + function test_requireRegisteredSpoke_canonicalTag() public { + _provider.setCanonicalSpoke('MAIN', SPOKE); + _harness.requireRegisteredSpoke(_provider, SPOKE); + } + + function test_requireRegisteredSpoke_tokenizationTag() public { + _provider.setTokenizationSpoke('MAIN', SPOKE); + _harness.requireRegisteredSpoke(_provider, SPOKE); + } + + function test_requireRegisteredSpoke_treasuryTag() public { + _provider.setTreasurySpoke('MAIN', SPOKE); + _harness.requireRegisteredSpoke(_provider, SPOKE); + } + + function test_requireRegisteredSpoke_revertsWith_SpokeNotRegistered() public { + vm.expectRevert(abi.encodeWithSelector(EngineUtils.SpokeNotRegistered.selector, SPOKE)); + _harness.requireRegisteredSpoke(_provider, SPOKE); + } + + function test_requireRegisteredSpoke_revertsWith_SpokeNotRegistered_hubTag() public { + _provider.setCanonicalHub('MAIN', SPOKE); + + vm.expectRevert(abi.encodeWithSelector(EngineUtils.SpokeNotRegistered.selector, SPOKE)); + _harness.requireRegisteredSpoke(_provider, SPOKE); + } + + function test_requireRegisteredCanonicalSpoke() public { + _provider.setCanonicalSpoke('MAIN', SPOKE); + _harness.requireRegisteredCanonicalSpoke(_provider, SPOKE); + } + + function test_requireRegisteredCanonicalSpoke_revertsWith_CanonicalSpokeNotRegistered() public { + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, SPOKE) + ); + _harness.requireRegisteredCanonicalSpoke(_provider, SPOKE); + } + + function test_requireRegisteredCanonicalSpoke_revertsWith_CanonicalSpokeNotRegistered_tokenizationTag() + public + { + _provider.setTokenizationSpoke('MAIN', SPOKE); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, SPOKE) + ); + _harness.requireRegisteredCanonicalSpoke(_provider, SPOKE); + } + + function test_requireRegisteredCanonicalSpoke_revertsWith_CanonicalSpokeNotRegistered_treasuryTag() + public + { + _provider.setTreasurySpoke('MAIN', SPOKE); + + vm.expectRevert( + abi.encodeWithSelector(EngineUtils.CanonicalSpokeNotRegistered.selector, SPOKE) + ); + _harness.requireRegisteredCanonicalSpoke(_provider, SPOKE); + } +} diff --git a/tests/config-engine/GovernanceTopology.t.sol b/tests/config-engine/GovernanceTopology.t.sol index 5fb12064f..c604cd9fa 100644 --- a/tests/config-engine/GovernanceTopology.t.sol +++ b/tests/config-engine/GovernanceTopology.t.sol @@ -30,6 +30,12 @@ contract ConfigEngineGovernanceTopologyTest is BaseConfigEngineTest { // in production the Executor, not the payload or the engine, holds the configurator permissions vm.prank(ADMIN); accessManager.grantRole(Roles.HUB_CONFIGURATOR_DOMAIN_ADMIN_ROLE, address(executor), 0); + + // in production the Executor owns the AddressesProvider; provider writes run in its context + vm.prank(address(engine)); + AddressesProviderInstance(address(addressesProvider)).transferOwnership(address(executor)); + vm.prank(address(executor)); + AddressesProviderInstance(address(addressesProvider)).acceptOwnership(); } function _executePayload(address target) internal { diff --git a/tests/config-engine/HubEngine.t.sol b/tests/config-engine/HubEngine.t.sol index 5d6194d96..63f8250dd 100644 --- a/tests/config-engine/HubEngine.t.sol +++ b/tests/config-engine/HubEngine.t.sol @@ -600,6 +600,7 @@ contract HubEngineTest is BaseConfigEngineTest { function test_executeHubSpokeToAssetsAdditions() public { (ISpoke newSpoke, ) = _deployNewSpoke(); + _registerSpokeOnProvider(newSpoke); IAaveV4ConfigEngine.SpokeAssetConfig[] memory assets = new IAaveV4ConfigEngine.SpokeAssetConfig[](2); @@ -762,7 +763,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: PROXY_ADMIN_OWNER, name: 'Tokenized NEW', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }); uint256 assetCountBefore = hub1().getAssetCount(); @@ -821,7 +823,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: PROXY_ADMIN_OWNER, name: 'Tokenized NEW', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }); address predictedProxy = TokenizationSpokeDeployer.computeProxyAddress( @@ -846,7 +849,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: PROXY_ADMIN_OWNER, name: '', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }); vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); @@ -860,7 +864,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: PROXY_ADMIN_OWNER, name: 'Tokenized NEW', - symbol: '' + symbol: '', + registrationName: 'TOKENIZED_NEW' }); vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); @@ -874,7 +879,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: address(0), name: '', - symbol: '' + symbol: '', + registrationName: '' }); vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); @@ -888,7 +894,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: address(0), name: 'Tokenized NEW', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }); vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); @@ -902,7 +909,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 0, proxyAdminOwner: PROXY_ADMIN_OWNER, name: '', - symbol: '' + symbol: '', + registrationName: '' }); vm.expectRevert(HubEngine.InvalidTokenizationSpokeConfig.selector); @@ -916,7 +924,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 0, proxyAdminOwner: PROXY_ADMIN_OWNER, name: 'Tokenized NEW', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }); uint256 assetCountBefore = hub1().getAssetCount(); @@ -1165,7 +1174,8 @@ contract HubEngineTest is BaseConfigEngineTest { addCap: 1000, proxyAdminOwner: PROXY_ADMIN_OWNER, name: 'Tokenized NEW', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }); engine.executeHubAssetListings(_toAssetListingArray(listing)); diff --git a/tests/contracts/addresses-provider/AddressesProvider.Upgradeable.t.sol b/tests/contracts/addresses-provider/AddressesProvider.Upgradeable.t.sol new file mode 100644 index 000000000..d644b0621 --- /dev/null +++ b/tests/contracts/addresses-provider/AddressesProvider.Upgradeable.t.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {Initializable} from 'src/dependencies/openzeppelin-upgradeable/Initializable.sol'; +import {OwnableUpgradeable} from 'src/dependencies/openzeppelin-upgradeable/OwnableUpgradeable.sol'; +import {Ownable2StepUpgradeable} from 'src/dependencies/openzeppelin-upgradeable/Ownable2StepUpgradeable.sol'; +import {IERC1967} from 'src/dependencies/openzeppelin/IERC1967.sol'; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {AddressesProviderInstance} from 'src/addresses-provider/instances/AddressesProviderInstance.sol'; +import {MockAddressesProviderInstance} from 'tests/helpers/mocks/MockAddressesProviderInstance.sol'; +import {ProxyHelpers} from 'tests/helpers/commons/ProxyHelpers.sol'; + +contract AddressesProviderUpgradeableTest is Test, ProxyHelpers { + address internal OWNER = makeAddr('OWNER'); + address internal proxyAdminOwner = makeAddr('proxyAdminOwner'); + + function test_implementation_constructor_fuzz(uint64 revision) public { + address implAddress = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + vm.expectEmit(implAddress); + emit Initializable.Initialized(type(uint64).max); + + MockAddressesProviderInstance impl = new MockAddressesProviderInstance(revision); + + assertEq(address(impl), implAddress); + assertEq(impl.ADDRESSES_PROVIDER_REVISION(), revision); + assertEq(_getProxyInitializedVersion(implAddress), type(uint64).max); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER); + } + + function test_proxy_constructor_fuzz(uint64 revision) public { + revision = uint64(bound(revision, 1, type(uint64).max)); + + MockAddressesProviderInstance impl = new MockAddressesProviderInstance(revision); + address proxyAddress = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + address proxyAdminAddress = vm.computeCreateAddress(proxyAddress, 1); + + vm.expectEmit(proxyAddress); + emit IERC1967.Upgraded(address(impl)); + vm.expectEmit(proxyAddress); + emit OwnableUpgradeable.OwnershipTransferred(address(0), OWNER); + vm.expectEmit(proxyAddress); + emit Initializable.Initialized(revision); + vm.expectEmit(proxyAdminAddress); + emit OwnableUpgradeable.OwnershipTransferred(address(0), proxyAdminOwner); + vm.expectEmit(proxyAddress); + emit IERC1967.AdminChanged(address(0), proxyAdminAddress); + + address proxy = _proxify(address(impl)); + + assertEq(proxy, proxyAddress); + assertEq(_getProxyAdminAddress(proxy), proxyAdminAddress); + assertEq(_getImplementationAddress(proxy), address(impl)); + + assertEq(_getProxyInitializedVersion(proxy), revision); + assertEq(Ownable2StepUpgradeable(proxy).owner(), OWNER); + } + + function test_proxy_reinitialization_fuzz(uint64 initialRevision) public { + initialRevision = uint64(bound(initialRevision, 1, type(uint64).max - 1)); + MockAddressesProviderInstance impl = new MockAddressesProviderInstance(initialRevision); + ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy(_proxify(address(impl))); + + uint64 secondRevision = uint64(vm.randomUint(initialRevision + 1, type(uint64).max)); + MockAddressesProviderInstance impl2 = new MockAddressesProviderInstance(secondRevision); + + vm.expectEmit(address(proxy)); + emit OwnableUpgradeable.OwnershipTransferred(OWNER, OWNER); + vm.prank(_getProxyAdminAddress(address(proxy))); + proxy.upgradeToAndCall( + address(impl2), + abi.encodeCall(MockAddressesProviderInstance.initialize, (OWNER)) + ); + + assertEq(Ownable2StepUpgradeable(address(proxy)).owner(), OWNER); + } + + function test_proxy_constructor_revertsWith_InvalidInitialization_ZeroRevision() public { + MockAddressesProviderInstance impl = new MockAddressesProviderInstance(0); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + _proxify(address(impl)); + } + + function test_proxy_constructor_fuzz_revertsWith_InvalidInitialization( + uint64 initialRevision + ) public { + initialRevision = uint64(bound(initialRevision, 1, type(uint64).max)); + + MockAddressesProviderInstance impl = new MockAddressesProviderInstance(initialRevision); + ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy(_proxify(address(impl))); + + vm.expectRevert(Initializable.InvalidInitialization.selector); + vm.prank(_getProxyAdminAddress(address(proxy))); + proxy.upgradeToAndCall( + address(impl), + abi.encodeCall(MockAddressesProviderInstance.initialize, (OWNER)) + ); + + uint64 secondRevision = uint64(vm.randomUint(0, initialRevision)); + MockAddressesProviderInstance impl2 = new MockAddressesProviderInstance(secondRevision); + vm.expectRevert(Initializable.InvalidInitialization.selector); + vm.prank(_getProxyAdminAddress(address(proxy))); + proxy.upgradeToAndCall( + address(impl2), + abi.encodeCall(MockAddressesProviderInstance.initialize, (OWNER)) + ); + } + + function test_proxy_constructor_revertsWith_InvalidAddress() public { + AddressesProviderInstance impl = new AddressesProviderInstance(); + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0)) + ); + new TransparentUpgradeableProxy( + address(impl), + proxyAdminOwner, + abi.encodeCall(AddressesProviderInstance.initialize, (address(0))) + ); + } + + function test_proxy_reinitialization_revertsWith_CallerNotProxyAdmin() public { + AddressesProviderInstance impl = new AddressesProviderInstance(); + ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy(_proxify(address(impl))); + + AddressesProviderInstance impl2 = new AddressesProviderInstance(); + vm.expectRevert(); + vm.prank(makeAddr('user')); + proxy.upgradeToAndCall( + address(impl2), + abi.encodeCall(AddressesProviderInstance.initialize, (OWNER)) + ); + } + + function _proxify(address impl) internal returns (address) { + return + address( + new TransparentUpgradeableProxy( + impl, + proxyAdminOwner, + abi.encodeCall(AddressesProviderInstance.initialize, (OWNER)) + ) + ); + } +} diff --git a/tests/contracts/addresses-provider/AddressesProvider.t.sol b/tests/contracts/addresses-provider/AddressesProvider.t.sol new file mode 100644 index 000000000..a992de393 --- /dev/null +++ b/tests/contracts/addresses-provider/AddressesProvider.t.sol @@ -0,0 +1,857 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; +import {OwnableUpgradeable} from 'src/dependencies/openzeppelin-upgradeable/OwnableUpgradeable.sol'; +import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol'; +import {AddressesProvider} from 'src/addresses-provider/AddressesProvider.sol'; +import {AddressesProviderInstance} from 'src/addresses-provider/instances/AddressesProviderInstance.sol'; +import {IAddressesProvider} from 'src/addresses-provider/interfaces/IAddressesProvider.sol'; + +contract AddressesProviderTest is Test { + address internal OWNER = makeAddr('OWNER'); + address internal PROXY_ADMIN_OWNER = makeAddr('PROXY_ADMIN_OWNER'); + + AddressesProvider internal provider; + + function setUp() public { + provider = AddressesProvider( + address( + new TransparentUpgradeableProxy( + address(new AddressesProviderInstance()), + PROXY_ADMIN_OWNER, + abi.encodeCall(AddressesProviderInstance.initialize, (OWNER)) + ) + ) + ); + } + + function _id(string memory name, string memory tag) internal pure returns (bytes32) { + return keccak256(abi.encode(name, tag)); + } + + function test_initialize() public view { + assertEq(provider.owner(), OWNER); + assertEq(provider.CANONICAL_HUB_TAG(), 'CANONICAL_HUB'); + assertEq(provider.CANONICAL_SPOKE_TAG(), 'CANONICAL_SPOKE'); + assertEq(provider.TOKENIZATION_SPOKE_TAG(), 'TOKENIZATION_SPOKE'); + assertEq(provider.TREASURY_SPOKE_TAG(), 'TREASURY_SPOKE'); + } + + function test_transferOwnership_twoStep() public { + address newOwner = makeAddr('NEW_OWNER'); + + vm.prank(OWNER); + provider.transferOwnership(newOwner); + + assertEq(provider.owner(), OWNER); + assertEq(provider.pendingOwner(), newOwner); + + vm.prank(newOwner); + provider.acceptOwnership(); + + assertEq(provider.owner(), newOwner); + assertEq(provider.pendingOwner(), address(0)); + } + + function test_getId() public view { + assertEq( + provider.getId({name: 'CORE', tag: provider.CANONICAL_HUB_TAG()}), + keccak256(abi.encode('CORE', 'CANONICAL_HUB')) + ); + assertEq( + provider.getId({name: 'MAIN', tag: provider.CANONICAL_SPOKE_TAG()}), + keccak256(abi.encode('MAIN', 'CANONICAL_SPOKE')) + ); + assertEq( + provider.getId({name: 'CORE_WETH', tag: provider.TOKENIZATION_SPOKE_TAG()}), + keccak256(abi.encode('CORE_WETH', 'TOKENIZATION_SPOKE')) + ); + assertEq( + provider.getId({name: 'MAIN', tag: provider.TREASURY_SPOKE_TAG()}), + keccak256(abi.encode('MAIN', 'TREASURY_SPOKE')) + ); + } + + function test_setEntry() public { + bytes32 id = _id('CONFIG_ENGINE', 'PERIPHERY'); + address configEngine = makeAddr('CONFIG_ENGINE'); + + vm.expectEmit(address(provider)); + emit IAddressesProvider.SetEntry(id, 'CONFIG_ENGINE', 'PERIPHERY', address(0), configEngine); + + vm.prank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + + assertEq(provider.getAddress(id), configEngine); + assertEq(provider.getAddress({name: 'CONFIG_ENGINE', tag: 'PERIPHERY'}), configEngine); + + IAddressesProvider.Entry memory entry = provider.getEntry(id); + assertEq(entry.addr, configEngine); + assertEq(entry.name, 'CONFIG_ENGINE'); + assertEq(entry.tag, 'PERIPHERY'); + + bytes32[] memory ids = provider.getIds('PERIPHERY'); + assertEq(ids.length, 1); + assertEq(ids[0], id); + + string[] memory tags = provider.getTags(); + assertEq(tags.length, 1); + assertEq(tags[0], 'PERIPHERY'); + + bytes32[] memory addressIds = provider.getAddressIds(configEngine); + assertEq(addressIds.length, 1); + assertEq(addressIds[0], id); + } + + function test_setEntry_remove() public { + bytes32 id = _id('CONFIG_ENGINE', 'PERIPHERY'); + address configEngine = makeAddr('CONFIG_ENGINE'); + + vm.startPrank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + vm.stopPrank(); + + assertEq(provider.getAddress(id), address(0)); + assertEq(provider.getEntry(id).tag, ''); + assertEq(provider.getEntry(id).name, ''); + assertEq(provider.getIds('PERIPHERY').length, 0); + assertEq(provider.getTags().length, 0); + assertEq(provider.getAddressIds(configEngine).length, 0); + } + + function test_setEntry_removeThenSet() public { + bytes32 id = _id('CONFIG_ENGINE', 'PERIPHERY'); + address newConfigEngine = makeAddr('NEW_CONFIG_ENGINE'); + + vm.startPrank(OWNER); + provider.setEntry({ + name: 'CONFIG_ENGINE', + tag: 'PERIPHERY', + newAddress: makeAddr('CONFIG_ENGINE') + }); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: newConfigEngine}); + vm.stopPrank(); + + assertEq(provider.getAddress(id), newConfigEngine); + + bytes32[] memory ids = provider.getIds('PERIPHERY'); + assertEq(ids.length, 1); + assertEq(ids[0], id); + } + + function test_setEntry_revertsWith_AddressAlreadySet() public { + bytes32 id = _id('CONFIG_ENGINE', 'PERIPHERY'); + address configEngine = makeAddr('CONFIG_ENGINE'); + + vm.startPrank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + + vm.expectRevert(abi.encodeWithSelector(IAddressesProvider.AddressAlreadySet.selector, id)); + provider.setEntry({ + name: 'CONFIG_ENGINE', + tag: 'PERIPHERY', + newAddress: makeAddr('NEW_CONFIG_ENGINE') + }); + + vm.expectRevert(abi.encodeWithSelector(IAddressesProvider.AddressAlreadySet.selector, id)); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + vm.stopPrank(); + } + + function test_setEntry_noIdCollision() public { + // With abi.encode, ('A_B', 'C') and ('A', 'B_C') resolve to distinct identifiers. + bytes32 firstId = _id('A_B', 'C'); + bytes32 secondId = _id('A', 'B_C'); + assertNotEq(firstId, secondId); + assertEq(provider.getId({name: 'A_B', tag: 'C'}), firstId); + assertEq(provider.getId({name: 'A', tag: 'B_C'}), secondId); + + address first = makeAddr('FIRST'); + address second = makeAddr('SECOND'); + + vm.startPrank(OWNER); + provider.setEntry({name: 'A_B', tag: 'C', newAddress: first}); + provider.setEntry({name: 'A', tag: 'B_C', newAddress: second}); + vm.stopPrank(); + + assertEq(provider.getAddress({name: 'A_B', tag: 'C'}), first); + assertEq(provider.getAddress({name: 'A', tag: 'B_C'}), second); + } + + function test_setEntry_sameAddressUnderMultipleIds() public { + address configEngine = makeAddr('CONFIG_ENGINE'); + + vm.startPrank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + provider.setEntry({name: 'ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'ENGINE', newAddress: configEngine}); + provider.setEntry({name: 'V3_CONFIG_ENGINE', tag: 'V3_PERIPHERY', newAddress: configEngine}); + vm.stopPrank(); + + assertEq(provider.getAddress({name: 'CONFIG_ENGINE', tag: 'PERIPHERY'}), configEngine); + assertEq(provider.getAddress({name: 'ENGINE', tag: 'PERIPHERY'}), configEngine); + assertEq(provider.getAddress({name: 'CONFIG_ENGINE', tag: 'ENGINE'}), configEngine); + assertEq(provider.getAddress({name: 'V3_CONFIG_ENGINE', tag: 'V3_PERIPHERY'}), configEngine); + + bytes32[] memory peripheryIds = provider.getIds('PERIPHERY'); + assertEq(peripheryIds.length, 2); + assertEq(peripheryIds[0], _id('CONFIG_ENGINE', 'PERIPHERY')); + assertEq(peripheryIds[1], _id('ENGINE', 'PERIPHERY')); + + string[] memory tags = provider.getTags(); + assertEq(tags.length, 3); + assertEq(tags[0], 'PERIPHERY'); + assertEq(tags[1], 'ENGINE'); + assertEq(tags[2], 'V3_PERIPHERY'); + + // the reverse map tracks every identifier the address is registered under + assertEq(provider.getAddressIdCount(configEngine), 4); + bytes32[] memory addressIds = provider.getAddressIds(configEngine); + assertEq(addressIds.length, 4); + assertEq(addressIds[0], _id('CONFIG_ENGINE', 'PERIPHERY')); + assertEq(addressIds[1], _id('ENGINE', 'PERIPHERY')); + assertEq(addressIds[2], _id('CONFIG_ENGINE', 'ENGINE')); + assertEq(addressIds[3], _id('V3_CONFIG_ENGINE', 'V3_PERIPHERY')); + + IAddressesProvider.Entry[] memory entries = provider.getEntries(configEngine); + assertEq(entries.length, 4); + assertEq(entries[0].name, 'CONFIG_ENGINE'); + assertEq(entries[0].tag, 'PERIPHERY'); + assertEq(entries[0].addr, configEngine); + assertEq(entries[3].name, 'V3_CONFIG_ENGINE'); + assertEq(entries[3].tag, 'V3_PERIPHERY'); + + // removing one entry does not affect the other entries of the same address + vm.prank(OWNER); + provider.setEntry({name: 'ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + + assertEq(provider.getAddress({name: 'ENGINE', tag: 'PERIPHERY'}), address(0)); + assertEq(provider.getAddress({name: 'CONFIG_ENGINE', tag: 'PERIPHERY'}), configEngine); + assertEq(provider.getAddress({name: 'CONFIG_ENGINE', tag: 'ENGINE'}), configEngine); + assertEq(provider.getIds('PERIPHERY').length, 1); + assertEq(provider.getAddressIdCount(configEngine), 3); + } + + function test_setHubAndSpoke_sameAddressAcrossTags() public { + address sharedSpoke = makeAddr('SHARED_SPOKE'); + + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', sharedSpoke); + provider.setTokenizationSpoke('MAIN', sharedSpoke); + provider.setTreasurySpoke('MAIN', sharedSpoke); + vm.stopPrank(); + + assertEq(provider.getCanonicalSpoke('MAIN'), sharedSpoke); + assertEq(provider.getTokenizationSpoke('MAIN'), sharedSpoke); + assertEq(provider.getTreasurySpoke('MAIN'), sharedSpoke); + + address[] memory canonicalSpokes = provider.getCanonicalSpokes(); + assertEq(canonicalSpokes.length, 1); + assertEq(canonicalSpokes[0], sharedSpoke); + + address[] memory tokenizationSpokes = provider.getTokenizationSpokes(); + assertEq(tokenizationSpokes.length, 1); + assertEq(tokenizationSpokes[0], sharedSpoke); + + address[] memory treasurySpokes = provider.getTreasurySpokes(); + assertEq(treasurySpokes.length, 1); + assertEq(treasurySpokes[0], sharedSpoke); + + IAddressesProvider.Entry[] memory entries = provider.getEntries(sharedSpoke); + assertEq(entries.length, 3); + assertEq(entries[0].tag, 'CANONICAL_SPOKE'); + assertEq(entries[1].tag, 'TOKENIZATION_SPOKE'); + assertEq(entries[2].tag, 'TREASURY_SPOKE'); + } + + function test_setEntry_remove_revertsWith_AddressNotSet() public { + bytes32 id = _id('CONFIG_ENGINE', 'PERIPHERY'); + + vm.startPrank(OWNER); + vm.expectRevert(abi.encodeWithSelector(IAddressesProvider.AddressNotSet.selector, id)); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + + provider.setEntry({ + name: 'CONFIG_ENGINE', + tag: 'PERIPHERY', + newAddress: makeAddr('CONFIG_ENGINE') + }); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + + vm.expectRevert(abi.encodeWithSelector(IAddressesProvider.AddressNotSet.selector, id)); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + vm.stopPrank(); + } + + function test_setEntry_revertsWith_InvalidName() public { + vm.expectRevert(IAddressesProvider.InvalidName.selector); + vm.prank(OWNER); + provider.setEntry({name: '', tag: 'PERIPHERY', newAddress: makeAddr('CONFIG_ENGINE')}); + } + + function test_setEntry_revertsWith_InvalidTag() public { + vm.expectRevert(IAddressesProvider.InvalidTag.selector); + vm.prank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: '', newAddress: makeAddr('CONFIG_ENGINE')}); + } + + function test_setEntry_revertsWith_OwnableUnauthorizedAccount() public { + address caller = makeAddr('caller'); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller) + ); + vm.prank(caller); + provider.setEntry({ + name: 'CONFIG_ENGINE', + tag: 'PERIPHERY', + newAddress: makeAddr('CONFIG_ENGINE') + }); + } + + function test_isRegistered() public { + address configEngine = makeAddr('CONFIG_ENGINE'); + + assertFalse(provider.isRegistered(configEngine, 'PERIPHERY')); + + vm.prank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + + assertTrue(provider.isRegistered(configEngine, 'PERIPHERY')); + assertFalse(provider.isRegistered(configEngine, 'MISC')); + assertFalse(provider.isRegistered(makeAddr('OTHER'), 'PERIPHERY')); + } + + function test_isRegistered_multipleTags() public { + address spoke = makeAddr('SPOKE'); + + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', spoke); + provider.setEntry({name: 'MAIN', tag: 'BABYLON', newAddress: spoke}); + vm.stopPrank(); + + assertTrue(provider.isRegistered(spoke, provider.CANONICAL_SPOKE_TAG())); + assertTrue(provider.isRegistered(spoke, 'BABYLON')); + assertFalse(provider.isRegistered(spoke, provider.CANONICAL_HUB_TAG())); + } + + function test_isRegistered_afterRemove() public { + address configEngine = makeAddr('CONFIG_ENGINE'); + + vm.startPrank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + vm.stopPrank(); + + assertFalse(provider.isRegistered(configEngine, 'PERIPHERY')); + } + + function test_isRegistered_multipleEntriesSameTag() public { + address configEngine = makeAddr('CONFIG_ENGINE'); + + vm.startPrank(OWNER); + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + provider.setEntry({name: 'ENGINE', tag: 'PERIPHERY', newAddress: configEngine}); + + provider.setEntry({name: 'CONFIG_ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + assertTrue(provider.isRegistered(configEngine, 'PERIPHERY')); + + provider.setEntry({name: 'ENGINE', tag: 'PERIPHERY', newAddress: address(0)}); + assertFalse(provider.isRegistered(configEngine, 'PERIPHERY')); + vm.stopPrank(); + } + + function test_setCanonicalHub() public { + address coreHub = makeAddr('CORE_HUB'); + address plusHub = makeAddr('PLUS_HUB'); + address primeHub = makeAddr('PRIME_HUB'); + + vm.startPrank(OWNER); + vm.expectEmit(address(provider)); + emit IAddressesProvider.SetEntry( + _id('CORE', 'CANONICAL_HUB'), + 'CORE', + 'CANONICAL_HUB', + address(0), + coreHub + ); + provider.setCanonicalHub('CORE', coreHub); + provider.setCanonicalHub('PLUS', plusHub); + provider.setCanonicalHub('PRIME', primeHub); + vm.stopPrank(); + + assertEq(provider.getCanonicalHub('CORE'), coreHub); + assertEq(provider.getCanonicalHub('PLUS'), plusHub); + assertEq(provider.getCanonicalHub('PRIME'), primeHub); + assertEq(provider.getAddress(_id('CORE', 'CANONICAL_HUB')), coreHub); + + IAddressesProvider.Entry memory entry = provider.getEntry(_id('CORE', 'CANONICAL_HUB')); + assertEq(entry.name, 'CORE'); + assertEq(entry.tag, 'CANONICAL_HUB'); + + bytes32[] memory hubIds = provider.getIds('CANONICAL_HUB'); + assertEq(hubIds.length, 3); + assertEq(hubIds[0], _id('CORE', 'CANONICAL_HUB')); + assertEq(hubIds[1], _id('PLUS', 'CANONICAL_HUB')); + assertEq(hubIds[2], _id('PRIME', 'CANONICAL_HUB')); + + address[] memory hubs = provider.getCanonicalHubs(); + assertEq(hubs.length, 3); + assertEq(hubs[0], coreHub); + assertEq(hubs[1], plusHub); + assertEq(hubs[2], primeHub); + assertEq(provider.getAddresses(provider.CANONICAL_HUB_TAG()), hubs); + } + + function test_setCanonicalHub_removeThenSet() public { + address coreHub = makeAddr('CORE_HUB'); + address newCoreHub = makeAddr('NEW_CORE_HUB'); + + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', coreHub); + provider.setCanonicalHub('CORE', address(0)); + + vm.expectEmit(address(provider)); + emit IAddressesProvider.SetEntry( + _id('CORE', 'CANONICAL_HUB'), + 'CORE', + 'CANONICAL_HUB', + address(0), + newCoreHub + ); + provider.setCanonicalHub('CORE', newCoreHub); + vm.stopPrank(); + + assertEq(provider.getCanonicalHub('CORE'), newCoreHub); + assertEq(provider.getCanonicalHubs().length, 1); + } + + function test_setCanonicalHub_revertsWith_AddressAlreadySet() public { + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', makeAddr('CORE_HUB')); + + vm.expectRevert( + abi.encodeWithSelector( + IAddressesProvider.AddressAlreadySet.selector, + _id('CORE', 'CANONICAL_HUB') + ) + ); + provider.setCanonicalHub('CORE', makeAddr('NEW_CORE_HUB')); + vm.stopPrank(); + } + + function test_setCanonicalHub_remove() public { + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', makeAddr('CORE_HUB')); + provider.setCanonicalHub('PLUS', makeAddr('PLUS_HUB')); + provider.setCanonicalHub('CORE', address(0)); + vm.stopPrank(); + + assertEq(provider.getCanonicalHub('CORE'), address(0)); + + address[] memory hubs = provider.getCanonicalHubs(); + assertEq(hubs.length, 1); + assertEq(hubs[0], provider.getCanonicalHub('PLUS')); + } + + function test_setCanonicalHub_remove_revertsWith_AddressNotSet() public { + vm.expectRevert( + abi.encodeWithSelector( + IAddressesProvider.AddressNotSet.selector, + _id('CORE', 'CANONICAL_HUB') + ) + ); + vm.prank(OWNER); + provider.setCanonicalHub('CORE', address(0)); + } + + function test_setCanonicalHub_revertsWith_InvalidName() public { + vm.expectRevert(IAddressesProvider.InvalidName.selector); + vm.prank(OWNER); + provider.setCanonicalHub('', makeAddr('CORE_HUB')); + } + + function test_setCanonicalHub_revertsWith_OwnableUnauthorizedAccount() public { + address caller = makeAddr('caller'); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller) + ); + vm.prank(caller); + provider.setCanonicalHub('CORE', makeAddr('CORE_HUB')); + } + + function test_setCanonicalHub_fuzz(string memory name, address hub) public { + vm.assume(bytes(name).length > 0); + vm.assume(hub != address(0)); + + vm.prank(OWNER); + provider.setCanonicalHub(name, hub); + + assertEq(provider.getCanonicalHub(name), hub); + + address[] memory hubs = provider.getCanonicalHubs(); + assertEq(hubs.length, 1); + assertEq(hubs[0], hub); + } + + function test_setCanonicalSpoke() public { + address mainSpoke = makeAddr('MAIN_SPOKE'); + address bluechipSpoke = makeAddr('BLUECHIP_SPOKE'); + address forexSpoke = makeAddr('FOREX_SPOKE'); + + vm.startPrank(OWNER); + vm.expectEmit(address(provider)); + emit IAddressesProvider.SetEntry( + _id('MAIN', 'CANONICAL_SPOKE'), + 'MAIN', + 'CANONICAL_SPOKE', + address(0), + mainSpoke + ); + provider.setCanonicalSpoke('MAIN', mainSpoke); + provider.setCanonicalSpoke('BLUECHIP', bluechipSpoke); + provider.setCanonicalSpoke('FOREX', forexSpoke); + vm.stopPrank(); + + assertEq(provider.getCanonicalSpoke('MAIN'), mainSpoke); + assertEq(provider.getCanonicalSpoke('BLUECHIP'), bluechipSpoke); + assertEq(provider.getCanonicalSpoke('FOREX'), forexSpoke); + assertEq(provider.getAddress(_id('MAIN', 'CANONICAL_SPOKE')), mainSpoke); + + address[] memory canonicalSpokes = provider.getCanonicalSpokes(); + assertEq(canonicalSpokes.length, 3); + assertEq(canonicalSpokes[0], mainSpoke); + assertEq(canonicalSpokes[1], bluechipSpoke); + assertEq(canonicalSpokes[2], forexSpoke); + } + + function test_setTokenizationSpoke() public { + address coreWethSpoke = makeAddr('CORE_WETH_TOKENIZATION_SPOKE'); + address primeGhoSpoke = makeAddr('PRIME_GHO_TOKENIZATION_SPOKE'); + + vm.startPrank(OWNER); + provider.setTokenizationSpoke('CORE_WETH', coreWethSpoke); + provider.setTokenizationSpoke('PRIME_GHO', primeGhoSpoke); + vm.stopPrank(); + + assertEq(provider.getTokenizationSpoke('CORE_WETH'), coreWethSpoke); + assertEq(provider.getTokenizationSpoke('PRIME_GHO'), primeGhoSpoke); + assertEq(provider.getAddress(_id('CORE_WETH', 'TOKENIZATION_SPOKE')), coreWethSpoke); + + address[] memory tokenizationSpokes = provider.getTokenizationSpokes(); + assertEq(tokenizationSpokes.length, 2); + assertEq(tokenizationSpokes[0], coreWethSpoke); + assertEq(tokenizationSpokes[1], primeGhoSpoke); + } + + function test_setTreasurySpoke() public { + address treasurySpoke = makeAddr('TREASURY_SPOKE'); + + vm.prank(OWNER); + provider.setTreasurySpoke('MAIN', treasurySpoke); + + assertEq(provider.getTreasurySpoke('MAIN'), treasurySpoke); + assertEq(provider.getAddress(_id('MAIN', 'TREASURY_SPOKE')), treasurySpoke); + + address[] memory treasurySpokes = provider.getTreasurySpokes(); + assertEq(treasurySpokes.length, 1); + assertEq(treasurySpokes[0], treasurySpoke); + } + + function test_setSpoke_sameNameAcrossTags() public { + address mainSpoke = makeAddr('MAIN_SPOKE'); + address treasurySpoke = makeAddr('TREASURY_SPOKE'); + + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', mainSpoke); + provider.setTreasurySpoke('MAIN', treasurySpoke); + vm.stopPrank(); + + assertEq(provider.getCanonicalSpoke('MAIN'), mainSpoke); + assertEq(provider.getTreasurySpoke('MAIN'), treasurySpoke); + } + + function test_setSpoke_removeThenSet() public { + address mainSpoke = makeAddr('MAIN_SPOKE'); + address newMainSpoke = makeAddr('NEW_MAIN_SPOKE'); + + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', mainSpoke); + provider.setCanonicalSpoke('MAIN', address(0)); + provider.setCanonicalSpoke('MAIN', newMainSpoke); + vm.stopPrank(); + + assertEq(provider.getCanonicalSpoke('MAIN'), newMainSpoke); + assertEq(provider.getCanonicalSpokes().length, 1); + } + + function test_setSpoke_revertsWith_AddressAlreadySet() public { + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', makeAddr('MAIN_SPOKE')); + + vm.expectRevert( + abi.encodeWithSelector( + IAddressesProvider.AddressAlreadySet.selector, + _id('MAIN', 'CANONICAL_SPOKE') + ) + ); + provider.setCanonicalSpoke('MAIN', makeAddr('NEW_MAIN_SPOKE')); + vm.stopPrank(); + } + + function test_setSpoke_remove() public { + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', makeAddr('MAIN_SPOKE')); + provider.setCanonicalSpoke('BLUECHIP', makeAddr('BLUECHIP_SPOKE')); + provider.setTreasurySpoke('MAIN', makeAddr('TREASURY_SPOKE')); + + provider.setCanonicalSpoke('MAIN', address(0)); + vm.stopPrank(); + + assertEq(provider.getCanonicalSpoke('MAIN'), address(0)); + + address[] memory canonicalSpokes = provider.getCanonicalSpokes(); + assertEq(canonicalSpokes.length, 1); + assertEq(canonicalSpokes[0], provider.getCanonicalSpoke('BLUECHIP')); + assertEq(provider.getTreasurySpokes().length, 1); + } + + function test_setSpoke_removeLastIdOfTag() public { + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', makeAddr('MAIN_SPOKE')); + provider.setTreasurySpoke('MAIN', makeAddr('TREASURY_SPOKE')); + + provider.setTreasurySpoke('MAIN', address(0)); + vm.stopPrank(); + + assertEq(provider.getTreasurySpokes().length, 0); + + string[] memory tags = provider.getTags(); + assertEq(tags.length, 1); + assertEq(tags[0], 'CANONICAL_SPOKE'); + } + + function test_setSpoke_remove_revertsWith_AddressNotSet() public { + vm.expectRevert( + abi.encodeWithSelector( + IAddressesProvider.AddressNotSet.selector, + _id('MAIN', 'CANONICAL_SPOKE') + ) + ); + vm.prank(OWNER); + provider.setCanonicalSpoke('MAIN', address(0)); + } + + function test_setSpoke_revertsWith_InvalidName() public { + vm.startPrank(OWNER); + + vm.expectRevert(IAddressesProvider.InvalidName.selector); + provider.setCanonicalSpoke('', makeAddr('MAIN_SPOKE')); + + vm.expectRevert(IAddressesProvider.InvalidName.selector); + provider.setTokenizationSpoke('', makeAddr('CORE_WETH_TOKENIZATION_SPOKE')); + + vm.expectRevert(IAddressesProvider.InvalidName.selector); + provider.setTreasurySpoke('', makeAddr('TREASURY_SPOKE')); + + vm.stopPrank(); + } + + function test_setSpoke_revertsWith_OwnableUnauthorizedAccount() public { + address caller = makeAddr('caller'); + vm.startPrank(caller); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller) + ); + provider.setCanonicalSpoke('MAIN', makeAddr('MAIN_SPOKE')); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller) + ); + provider.setTokenizationSpoke('CORE_WETH', makeAddr('CORE_WETH_TOKENIZATION_SPOKE')); + + vm.expectRevert( + abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, caller) + ); + provider.setTreasurySpoke('MAIN', makeAddr('TREASURY_SPOKE')); + + vm.stopPrank(); + } + + function test_setSpoke_fuzz(string memory name, address spoke) public { + vm.assume(bytes(name).length > 0); + vm.assume(spoke != address(0)); + + vm.prank(OWNER); + provider.setCanonicalSpoke(name, spoke); + + assertEq(provider.getCanonicalSpoke(name), spoke); + + address[] memory canonicalSpokes = provider.getCanonicalSpokes(); + assertEq(canonicalSpokes.length, 1); + assertEq(canonicalSpokes[0], spoke); + } + + function test_getTags() public { + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', makeAddr('CORE_HUB')); + provider.setCanonicalSpoke('MAIN', makeAddr('MAIN_SPOKE')); + provider.setTokenizationSpoke('CORE_WETH', makeAddr('CORE_WETH_TOKENIZATION_SPOKE')); + provider.setTreasurySpoke('MAIN', makeAddr('TREASURY_SPOKE')); + vm.stopPrank(); + + assertEq(provider.getTagCount(), 4); + + string[] memory tags = provider.getTags(); + assertEq(tags.length, 4); + assertEq(tags[0], 'CANONICAL_HUB'); + assertEq(tags[1], 'CANONICAL_SPOKE'); + assertEq(tags[2], 'TOKENIZATION_SPOKE'); + assertEq(tags[3], 'TREASURY_SPOKE'); + } + + function test_getTags_bounded() public { + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', makeAddr('CORE_HUB')); + provider.setCanonicalSpoke('MAIN', makeAddr('MAIN_SPOKE')); + provider.setTokenizationSpoke('CORE_WETH', makeAddr('CORE_WETH_TOKENIZATION_SPOKE')); + provider.setTreasurySpoke('MAIN', makeAddr('TREASURY_SPOKE')); + vm.stopPrank(); + + string[] memory firstTwo = provider.getTags(0, 2); + assertEq(firstTwo.length, 2); + assertEq(firstTwo[0], 'CANONICAL_HUB'); + assertEq(firstTwo[1], 'CANONICAL_SPOKE'); + + string[] memory lastTwo = provider.getTags(2, 4); + assertEq(lastTwo.length, 2); + assertEq(lastTwo[0], 'TOKENIZATION_SPOKE'); + assertEq(lastTwo[1], 'TREASURY_SPOKE'); + + // end is capped to the number of tags + string[] memory clamped = provider.getTags(3, 100); + assertEq(clamped.length, 1); + assertEq(clamped[0], 'TREASURY_SPOKE'); + + // start beyond the number of tags yields an empty slice + assertEq(provider.getTags(10, 20).length, 0); + } + + function test_getIds_bounded() public { + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', makeAddr('CORE_HUB')); + provider.setCanonicalHub('PLUS', makeAddr('PLUS_HUB')); + provider.setCanonicalHub('PRIME', makeAddr('PRIME_HUB')); + vm.stopPrank(); + + assertEq(provider.getIdCount('CANONICAL_HUB'), 3); + + bytes32[] memory firstTwo = provider.getIds('CANONICAL_HUB', 0, 2); + assertEq(firstTwo.length, 2); + assertEq(firstTwo[0], _id('CORE', 'CANONICAL_HUB')); + assertEq(firstTwo[1], _id('PLUS', 'CANONICAL_HUB')); + + bytes32[] memory last = provider.getIds('CANONICAL_HUB', 2, 100); + assertEq(last.length, 1); + assertEq(last[0], _id('PRIME', 'CANONICAL_HUB')); + + assertEq(provider.getIds('CANONICAL_HUB', 5, 10).length, 0); + } + + function test_getAddresses_bounded() public { + address coreHub = makeAddr('CORE_HUB'); + address plusHub = makeAddr('PLUS_HUB'); + address primeHub = makeAddr('PRIME_HUB'); + + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', coreHub); + provider.setCanonicalHub('PLUS', plusHub); + provider.setCanonicalHub('PRIME', primeHub); + vm.stopPrank(); + + address[] memory firstTwo = provider.getAddresses('CANONICAL_HUB', 0, 2); + assertEq(firstTwo.length, 2); + assertEq(firstTwo[0], coreHub); + assertEq(firstTwo[1], plusHub); + + address[] memory last = provider.getAddresses('CANONICAL_HUB', 2, 100); + assertEq(last.length, 1); + assertEq(last[0], primeHub); + } + + function test_getCanonicalHubs_bounded() public { + address coreHub = makeAddr('CORE_HUB'); + address plusHub = makeAddr('PLUS_HUB'); + address primeHub = makeAddr('PRIME_HUB'); + + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', coreHub); + provider.setCanonicalHub('PLUS', plusHub); + provider.setCanonicalHub('PRIME', primeHub); + vm.stopPrank(); + + address[] memory firstTwo = provider.getCanonicalHubs(0, 2); + assertEq(firstTwo.length, 2); + assertEq(firstTwo[0], coreHub); + assertEq(firstTwo[1], plusHub); + + address[] memory last = provider.getCanonicalHubs(2, 100); + assertEq(last.length, 1); + assertEq(last[0], primeHub); + } + + function test_getSpokes_bounded() public { + address mainSpoke = makeAddr('MAIN_SPOKE'); + address extraSpoke = makeAddr('EXTRA_SPOKE'); + address tokenizationSpoke = makeAddr('TOKENIZATION_SPOKE'); + address treasurySpoke = makeAddr('TREASURY_SPOKE'); + + vm.startPrank(OWNER); + provider.setCanonicalSpoke('MAIN', mainSpoke); + provider.setCanonicalSpoke('EXTRA', extraSpoke); + provider.setTokenizationSpoke('CORE_WETH', tokenizationSpoke); + provider.setTreasurySpoke('MAIN', treasurySpoke); + vm.stopPrank(); + + address[] memory canonicalSpokes = provider.getCanonicalSpokes(1, 100); + assertEq(canonicalSpokes.length, 1); + assertEq(canonicalSpokes[0], extraSpoke); + + address[] memory tokenizationSpokes = provider.getTokenizationSpokes(0, 100); + assertEq(tokenizationSpokes.length, 1); + assertEq(tokenizationSpokes[0], tokenizationSpoke); + + address[] memory treasurySpokes = provider.getTreasurySpokes(0, 100); + assertEq(treasurySpokes.length, 1); + assertEq(treasurySpokes[0], treasurySpoke); + } + + function test_getAddressIds_bounded() public { + address shared = makeAddr('SHARED'); + + vm.startPrank(OWNER); + provider.setCanonicalHub('CORE', shared); + provider.setCanonicalSpoke('MAIN', shared); + provider.setTreasurySpoke('MAIN', shared); + vm.stopPrank(); + + assertEq(provider.getAddressIdCount(shared), 3); + + bytes32[] memory firstTwo = provider.getAddressIds(shared, 0, 2); + assertEq(firstTwo.length, 2); + assertEq(firstTwo[0], _id('CORE', 'CANONICAL_HUB')); + assertEq(firstTwo[1], _id('MAIN', 'CANONICAL_SPOKE')); + + IAddressesProvider.Entry[] memory entries = provider.getEntries(shared, 1, 3); + assertEq(entries.length, 2); + assertEq(entries[0].tag, 'CANONICAL_SPOKE'); + assertEq(entries[1].tag, 'TREASURY_SPOKE'); + + assertEq(provider.getAddressIds(shared, 5, 10).length, 0); + } +} diff --git a/tests/deployments/procedures/ProceduresBase.t.sol b/tests/deployments/procedures/ProceduresBase.t.sol index 1e21a15e5..62c610e91 100644 --- a/tests/deployments/procedures/ProceduresBase.t.sol +++ b/tests/deployments/procedures/ProceduresBase.t.sol @@ -16,6 +16,7 @@ import {AaveV4AccessManagerEnumerableDeployProcedureWrapper} from 'tests/helpers import {AaveV4AaveOracleDeployProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AaveV4AaveOracleDeployProcedureWrapper.sol'; import {AaveV4SpokeDeployProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AaveV4SpokeDeployProcedureWrapper.sol'; import {AaveV4TreasurySpokeDeployProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AaveV4TreasurySpokeDeployProcedureWrapper.sol'; +import {AddressesProviderDeployProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AddressesProviderDeployProcedureWrapper.sol'; import {AaveV4SpokeConfiguratorDeployProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AaveV4SpokeConfiguratorDeployProcedureWrapper.sol'; import {AaveV4AccessManagerRolesProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AaveV4AccessManagerRolesProcedureWrapper.sol'; import {AaveV4SpokeRolesProcedureWrapper} from 'tests/helpers/mocks/deployments/procedures/AaveV4SpokeRolesProcedureWrapper.sol'; diff --git a/tests/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.t.sol b/tests/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.t.sol new file mode 100644 index 000000000..88c80828a --- /dev/null +++ b/tests/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.t.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import 'tests/deployments/procedures/ProceduresBase.t.sol'; + +contract AddressesProviderDeployProcedureTest is ProceduresBase { + AddressesProviderDeployProcedureWrapper public addressesProviderDeployProcedureWrapper; + + function setUp() public override { + super.setUp(); + addressesProviderDeployProcedureWrapper = new AddressesProviderDeployProcedureWrapper(); + } + + function test_deployAddressesProvider() public { + ( + address addressesProviderProxy, + address addressesProviderImplementation + ) = addressesProviderDeployProcedureWrapper.deployAddressesProvider(owner, salt); + assertEq(Ownable(addressesProviderProxy).owner(), owner); + assertEq(Ownable(ProxyHelper.getProxyAdmin(addressesProviderProxy)).owner(), owner); + assertNotEq(addressesProviderImplementation, address(0)); + assertEq( + ProxyHelper.getImplementation(addressesProviderProxy), + addressesProviderImplementation + ); + } + + function test_deployAddressesProvider_reverts() public { + vm.expectRevert('invalid owner'); + addressesProviderDeployProcedureWrapper.deployAddressesProvider({ + owner: address(0), + salt: salt + }); + } +} diff --git a/tests/helpers/mocks/MockAddressesProviderInstance.sol b/tests/helpers/mocks/MockAddressesProviderInstance.sol new file mode 100644 index 000000000..cfafc3f38 --- /dev/null +++ b/tests/helpers/mocks/MockAddressesProviderInstance.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AddressesProvider} from 'src/addresses-provider/AddressesProvider.sol'; + +contract MockAddressesProviderInstance is AddressesProvider { + bool public constant IS_TEST = true; + + uint64 public immutable ADDRESSES_PROVIDER_REVISION; + + /** + * @dev Constructor. + * @dev It sets the addresses provider revision and disables the initializers. + * @param addressesProviderRevision_ The revision of the addresses provider contract. + */ + constructor(uint64 addressesProviderRevision_) { + ADDRESSES_PROVIDER_REVISION = addressesProviderRevision_; + _disableInitializers(); + } + + /// @inheritdoc AddressesProvider + function initialize(address owner) external override reinitializer(ADDRESSES_PROVIDER_REVISION) { + __Ownable_init(owner); + __Ownable2Step_init(); + } +} diff --git a/tests/helpers/mocks/config-engine/AaveV4PayloadWrapper.sol b/tests/helpers/mocks/config-engine/AaveV4PayloadWrapper.sol index 32854a2f5..955c7525d 100644 --- a/tests/helpers/mocks/config-engine/AaveV4PayloadWrapper.sol +++ b/tests/helpers/mocks/config-engine/AaveV4PayloadWrapper.sol @@ -14,6 +14,9 @@ contract AaveV4PayloadWrapper is AaveV4Payload { uint256 public postExecuteOrder; uint256 private _callCounter; + // AddressesProvider action storage + IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] private _addressesProviderEntryUpdates; + // Hub action storage IAaveV4ConfigEngine.AssetListing[] private _hubAssetListings; IAaveV4ConfigEngine.AssetConfigUpdate[] private _hubAssetConfigUpdates; @@ -56,6 +59,16 @@ contract AaveV4PayloadWrapper is AaveV4Payload { postExecuteOrder = ++_callCounter; } + // AddressesProvider setters + function setAddressesProviderEntryUpdates( + IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] memory items + ) external { + delete _addressesProviderEntryUpdates; + for (uint256 i = 0; i < items.length; i++) { + _addressesProviderEntryUpdates.push(items[i]); + } + } + // Hub setters function setHubAssetListings(IAaveV4ConfigEngine.AssetListing[] memory items) external { delete _hubAssetListings; @@ -223,6 +236,15 @@ contract AaveV4PayloadWrapper is AaveV4Payload { } } + function addressesProviderEntryUpdates() + public + view + override + returns (IAaveV4ConfigEngine.AddressesProviderEntryUpdate[] memory) + { + return _addressesProviderEntryUpdates; + } + function hubAssetListings() public view diff --git a/tests/helpers/mocks/config-engine/MockTokenizationListingPayload.sol b/tests/helpers/mocks/config-engine/MockTokenizationListingPayload.sol index 831c97303..656c1f671 100644 --- a/tests/helpers/mocks/config-engine/MockTokenizationListingPayload.sol +++ b/tests/helpers/mocks/config-engine/MockTokenizationListingPayload.sol @@ -57,7 +57,8 @@ contract MockTokenizationListingPayload is AaveV4Payload { addCap: 1000, proxyAdminOwner: PROXY_ADMIN_OWNER, name: 'Tokenized NEW', - symbol: 'tNEW' + symbol: 'tNEW', + registrationName: 'TOKENIZED_NEW' }) }); return listings; diff --git a/tests/helpers/mocks/deployments/procedures/AddressesProviderDeployProcedureWrapper.sol b/tests/helpers/mocks/deployments/procedures/AddressesProviderDeployProcedureWrapper.sol new file mode 100644 index 000000000..26891b0a6 --- /dev/null +++ b/tests/helpers/mocks/deployments/procedures/AddressesProviderDeployProcedureWrapper.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {AddressesProviderDeployProcedure} from 'src/deployments/procedures/deploy/addresses-provider/AddressesProviderDeployProcedure.sol'; + +contract AddressesProviderDeployProcedureWrapper is AddressesProviderDeployProcedure { + bool public IS_TEST = true; + + function deployAddressesProvider( + address owner, + bytes32 salt + ) external returns (address, address) { + return _deployAddressesProvider(owner, salt); + } +}