From 79b4483d605a6373eadfcb01e06e2e82ed88bad2 Mon Sep 17 00:00:00 2001 From: Alexandru Niculae <43644109+avniculae@users.noreply.github.com> Date: Thu, 14 May 2026 18:55:13 +0300 Subject: [PATCH 1/6] feat: deploy script for USDG Correlated Spoke --- .../AaveV4DeployUSDGCorrelatedSpoke.s.sol | 112 ++++++++++++++++++ .../AaveV4DeployUSDGCorrelatedSpoke.t.sol | 75 ++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol create mode 100644 tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol diff --git a/scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol b/scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol new file mode 100644 index 000000000..0d92ad78b --- /dev/null +++ b/scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {Script} from 'forge-std/Script.sol'; +import {console2 as console} from 'forge-std/console2.sol'; + +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; +import {AaveV4DeployOrchestration} from 'src/deployments/orchestration/AaveV4DeployOrchestration.sol'; +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.sol'; +import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; + +/// Usage (make sure FOUNDRY_LIBRARIES is populated in .env with LiquidationLibrary address): +/// FOUNDRY_PROFILE=mainnet forge clean && forge script \ +/// scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol:AaveV4DeployUSDGCorrelatedSpoke \ +/// --rpc-url mainnet --account --slow (--broadcast --verify) +abstract contract AaveV4DeployUSDGCorrelatedSpokeBase is Script { + struct SpokeDeployInputs { + address proxyAdminOwner; + address authority; + uint8 oracleDecimals; + uint16 maxUserReservesLimit; + bytes32 salt; + } + + function _getDeployInputs( + address deployer + ) internal view virtual returns (SpokeDeployInputs memory); + + function _expectedChainId() internal view virtual returns (uint256); + + function run() external virtual returns (BatchReports.SpokeInstanceBatchReport memory report) { + require(block.chainid == _expectedChainId(), 'chain id mismatch'); + + vm.startBroadcast(); + (, address deployer, ) = vm.readCallers(); + SpokeDeployInputs memory inputs = _getDeployInputs(deployer); + report = _deploy(inputs); + vm.stopBroadcast(); + + _logReport(deployer, inputs, report); + } + + function _deploy( + SpokeDeployInputs memory inputs + ) internal returns (BatchReports.SpokeInstanceBatchReport memory) { + return + AaveV4DeployBase.deploySpokeInstanceBatch({ + proxyAdminOwner: inputs.proxyAdminOwner, + authority: inputs.authority, + spokeBytecode: BytecodeHelper.getSpokeBytecode(), + oracleDecimals: inputs.oracleDecimals, + maxUserReservesLimit: inputs.maxUserReservesLimit, + salt: inputs.salt + }); + } + + function _logReport( + address deployer, + SpokeDeployInputs memory inputs, + BatchReports.SpokeInstanceBatchReport memory report + ) internal pure { + console.log('USDG Correlated Spoke deployment complete'); + console.log(' deployer :', deployer); + console.log(' authority :', inputs.authority); + console.log(' proxyAdminOwner :', inputs.proxyAdminOwner); + console.log(' oracleDecimals :', uint256(inputs.oracleDecimals)); + console.log(' maxUserReservesLimit :', uint256(inputs.maxUserReservesLimit)); + console.log(' spokeProxy :', report.spokeProxy); + console.log(' spokeImpl :', report.spokeImplementation); + console.log(' aaveOracle :', report.aaveOracle); + } +} + +contract AaveV4DeployUSDGCorrelatedSpoke is AaveV4DeployUSDGCorrelatedSpokeBase { + uint256 internal constant _ETHEREUM_CHAIN_ID = 1; + + // AaveV4Ethereum.ACCESS_MANAGER + // https://github.com/aave-dao/aave-address-book/blob/c48a741a10b94202f738d52a09e9c9a8bf18a67d/src/AaveV4Ethereum.sol#L8 + address public constant ACCESS_MANAGER = 0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01; + // GovernanceV3Ethereum.EXECUTOR_LVL_1 + // https://github.com/aave-dao/aave-address-book/blob/c48a741a10b94202f738d52a09e9c9a8bf18a67d/src/GovernanceV3Ethereum.sol#L56 + address public constant EXECUTOR_LVL_1 = 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A; + + uint256 internal constant _VERSION = 1; + string internal constant _SPOKE_LABEL = 'USDG_CORRELATED_SPOKE'; + + function spokeSalt(address deployer) public view returns (bytes32) { + bytes32 userSalt = keccak256( + bytes(string.concat('chain ', vm.toString(block.chainid), '_version ', vm.toString(_VERSION))) + ); + bytes32 rootSalt = AaveV4DeployOrchestration._deriveSalt(deployer, userSalt); + return AaveV4DeployOrchestration._deriveChildSalt(rootSalt, 'spoke', _SPOKE_LABEL); + } + + function _getDeployInputs( + address deployer + ) internal view override returns (SpokeDeployInputs memory) { + return + SpokeDeployInputs({ + proxyAdminOwner: EXECUTOR_LVL_1, + authority: ACCESS_MANAGER, + oracleDecimals: DeployConstants.ORACLE_DECIMALS, + maxUserReservesLimit: DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT, + salt: spokeSalt(deployer) + }); + } + + function _expectedChainId() internal pure override returns (uint256) { + return _ETHEREUM_CHAIN_ID; + } +} diff --git a/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol b/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol new file mode 100644 index 000000000..6f3488143 --- /dev/null +++ b/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPriceOracle} from 'src/spoke/interfaces/IPriceOracle.sol'; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; + +import {AaveV4DeployUSDGCorrelatedSpoke} from 'scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol'; + +contract AaveV4DeployUSDGCorrelatedSpokeTest is Test { + AaveV4DeployUSDGCorrelatedSpoke internal _script; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 25092080); + _script = new AaveV4DeployUSDGCorrelatedSpoke(); + } + + function test_run_deploysSpoke() public { + BatchReports.SpokeInstanceBatchReport memory report = _script.run(); + + assertGt(report.spokeProxy.code.length, 0); + assertGt(report.spokeImplementation.code.length, 0); + assertGt(report.aaveOracle.code.length, 0); + + assertEq(IAccessManaged(report.spokeProxy).authority(), _script.ACCESS_MANAGER()); + assertEq(ISpoke(report.spokeProxy).ORACLE(), report.aaveOracle); + assertEq(IPriceOracle(report.aaveOracle).spoke(), report.spokeProxy); + assertEq( + uint256(IPriceOracle(report.aaveOracle).decimals()), + uint256(DeployConstants.ORACLE_DECIMALS) + ); + assertEq( + uint256(ISpoke(report.spokeProxy).MAX_USER_RESERVES_LIMIT()), + uint256(DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT) + ); + } + + // Same salt does NOT collide: each batch deploys a fresh CREATE-allocated AaveOracle whose + // address is in SpokeInstance's init code, so the CREATE2 spoke address differs across calls. + // Operator must avoid running the script twice — no on-chain safety check. + function test_run_repeatCallsProduceDistinctSpokes() public { + BatchReports.SpokeInstanceBatchReport memory a = _script.run(); + BatchReports.SpokeInstanceBatchReport memory b = _script.run(); + assertNotEq(a.spokeProxy, b.spokeProxy); + assertNotEq(a.aaveOracle, b.aaveOracle); + } + + function test_run_revertsOffMainnet_fuzz(uint64 wrongChainId) public { + vm.assume(wrongChainId != 1); + vm.chainId(wrongChainId); + + vm.expectRevert('chain id mismatch'); + _script.run(); + } + + function test_constantsMatchAddressBook() public view { + assertEq(_script.ACCESS_MANAGER(), 0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01); + assertEq(_script.EXECUTOR_LVL_1(), 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A); + } + + function test_spokeSaltMatchesOrchestrationFormula_fuzz(address deployer) public view { + bytes32 orchestrationSalt = keccak256('AAVE_V4'); + bytes32 userSalt = keccak256(bytes('chain 1_version 1')); + bytes32 expectedRoot = bytes32(bytes20(deployer)) | + (keccak256(abi.encode(orchestrationSalt, userSalt)) >> 160); + bytes32 expected = keccak256(abi.encode(expectedRoot, 'spoke', 'USDG_CORRELATED_SPOKE')); + + assertEq(_script.spokeSalt(deployer), expected); + } +} From aa95364d21af520b5f5d0ac5df2d4e542b9f92aa Mon Sep 17 00:00:00 2001 From: Alexandru Niculae <43644109+avniculae@users.noreply.github.com> Date: Sun, 14 Jun 2026 23:36:52 +0300 Subject: [PATCH 2/6] feat: add script to deploy isolated hub --- ....sol => AaveV4DeployCorrelatedSpoke.s.sol} | 35 +++-- scripts/deploy/AaveV4DeployIsolatedHub.s.sol | 120 ++++++++++++++++++ 2 files changed, 147 insertions(+), 8 deletions(-) rename scripts/deploy/{AaveV4DeployUSDGCorrelatedSpoke.s.sol => AaveV4DeployCorrelatedSpoke.s.sol} (73%) create mode 100644 scripts/deploy/AaveV4DeployIsolatedHub.s.sol diff --git a/scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol b/scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol similarity index 73% rename from scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol rename to scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol index 0d92ad78b..80fa712a6 100644 --- a/scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol +++ b/scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol @@ -10,11 +10,14 @@ import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; import {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.sol'; import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; -/// Usage (make sure FOUNDRY_LIBRARIES is populated in .env with LiquidationLibrary address): -/// FOUNDRY_PROFILE=mainnet forge clean && forge script \ -/// scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol:AaveV4DeployUSDGCorrelatedSpoke \ -/// --rpc-url mainnet --account --slow (--broadcast --verify) -abstract contract AaveV4DeployUSDGCorrelatedSpokeBase is Script { +/// @title AaveV4DeployCorrelatedSpokeBase +/// @author Aave Labs +/// @notice Generic base script to deploy a standalone Spoke instance (proxy + implementation + AaveOracle) +/// intended for a correlated-asset market. Concrete scripts override the deploy inputs, the +/// expected chain id and the deployment name for a specific market. +/// @dev Requires FOUNDRY_LIBRARIES to be populated in .env with the LiquidationLogic library address, as +/// SpokeInstance depends on it. +abstract contract AaveV4DeployCorrelatedSpokeBase is Script { struct SpokeDeployInputs { address proxyAdminOwner; address authority; @@ -23,12 +26,17 @@ abstract contract AaveV4DeployUSDGCorrelatedSpokeBase is Script { bytes32 salt; } + /// @dev Override to provide the market-specific deploy inputs. function _getDeployInputs( address deployer ) internal view virtual returns (SpokeDeployInputs memory); + /// @dev Override to return the expected chain id for this deployment. function _expectedChainId() internal view virtual returns (uint256); + /// @dev Override to return a human-readable name for this spoke deployment (used in logs). + function _deploymentName() internal view virtual returns (string memory); + function run() external virtual returns (BatchReports.SpokeInstanceBatchReport memory report) { require(block.chainid == _expectedChainId(), 'chain id mismatch'); @@ -59,8 +67,8 @@ abstract contract AaveV4DeployUSDGCorrelatedSpokeBase is Script { address deployer, SpokeDeployInputs memory inputs, BatchReports.SpokeInstanceBatchReport memory report - ) internal pure { - console.log('USDG Correlated Spoke deployment complete'); + ) internal view { + console.log(string.concat(_deploymentName(), ' deployment complete')); console.log(' deployer :', deployer); console.log(' authority :', inputs.authority); console.log(' proxyAdminOwner :', inputs.proxyAdminOwner); @@ -72,7 +80,14 @@ abstract contract AaveV4DeployUSDGCorrelatedSpokeBase is Script { } } -contract AaveV4DeployUSDGCorrelatedSpoke is AaveV4DeployUSDGCorrelatedSpokeBase { +/// @title AaveV4DeployUSDGCorrelatedSpoke +/// @author Aave Labs +/// @notice Deploys the USDG correlated-asset Spoke on Ethereum mainnet. +/// @dev Usage (make sure FOUNDRY_LIBRARIES is populated in .env with the LiquidationLogic address): +/// forge clean && forge script \ +/// scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol:AaveV4DeployUSDGCorrelatedSpoke \ +/// --rpc-url mainnet --account --slow (--broadcast --verify) +contract AaveV4DeployUSDGCorrelatedSpoke is AaveV4DeployCorrelatedSpokeBase { uint256 internal constant _ETHEREUM_CHAIN_ID = 1; // AaveV4Ethereum.ACCESS_MANAGER @@ -109,4 +124,8 @@ contract AaveV4DeployUSDGCorrelatedSpoke is AaveV4DeployUSDGCorrelatedSpokeBase function _expectedChainId() internal pure override returns (uint256) { return _ETHEREUM_CHAIN_ID; } + + function _deploymentName() internal pure override returns (string memory) { + return 'USDG Correlated Spoke'; + } } diff --git a/scripts/deploy/AaveV4DeployIsolatedHub.s.sol b/scripts/deploy/AaveV4DeployIsolatedHub.s.sol new file mode 100644 index 000000000..e95ffa6e2 --- /dev/null +++ b/scripts/deploy/AaveV4DeployIsolatedHub.s.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {Script} from 'forge-std/Script.sol'; +import {console2 as console} from 'forge-std/console2.sol'; + +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; +import {AaveV4DeployOrchestration} from 'src/deployments/orchestration/AaveV4DeployOrchestration.sol'; +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {BytecodeHelper} from 'src/deployments/utils/libraries/BytecodeHelper.sol'; + +/// @title AaveV4DeployIsolatedHubBase +/// @author Aave Labs +/// @notice Generic base script to deploy a standalone Hub instance (proxy + implementation + interest rate +/// strategy) intended for an isolated market. Concrete scripts override the deploy inputs, the +/// expected chain id and the deployment name for a specific market. +abstract contract AaveV4DeployIsolatedHubBase is Script { + struct HubDeployInputs { + address proxyAdminOwner; + address authority; + bytes32 salt; + } + + /// @dev Override to provide the market-specific deploy inputs. + function _getDeployInputs( + address deployer + ) internal view virtual returns (HubDeployInputs memory); + + /// @dev Override to return the expected chain id for this deployment. + function _expectedChainId() internal view virtual returns (uint256); + + /// @dev Override to return a human-readable name for this hub deployment (used in logs). + function _deploymentName() internal view virtual returns (string memory); + + function run() external virtual returns (BatchReports.HubInstanceBatchReport memory report) { + require(block.chainid == _expectedChainId(), 'chain id mismatch'); + + vm.startBroadcast(); + (, address deployer, ) = vm.readCallers(); + HubDeployInputs memory inputs = _getDeployInputs(deployer); + report = _deploy(inputs); + vm.stopBroadcast(); + + _logReport(deployer, inputs, report); + } + + function _deploy( + HubDeployInputs memory inputs + ) internal returns (BatchReports.HubInstanceBatchReport memory) { + return + AaveV4DeployBase.deployHubInstanceBatch({ + proxyAdminOwner: inputs.proxyAdminOwner, + authority: inputs.authority, + hubBytecode: BytecodeHelper.getHubBytecode(), + salt: inputs.salt + }); + } + + function _logReport( + address deployer, + HubDeployInputs memory inputs, + BatchReports.HubInstanceBatchReport memory report + ) internal view { + console.log(string.concat(_deploymentName(), ' deployment complete')); + console.log(' deployer :', deployer); + console.log(' authority :', inputs.authority); + console.log(' proxyAdminOwner :', inputs.proxyAdminOwner); + console.log(' hubProxy :', report.hubProxy); + console.log(' hubImpl :', report.hubImplementation); + console.log(' interestRateStrategy :', report.irStrategy); + } +} + +/// @title AaveV4DeployPendlePaxosIsolatedHub +/// @author Aave Labs +/// @notice Deploys the Pendle Paxos isolated-market Hub on Ethereum mainnet. +/// @dev Usage (FOUNDRY_LIBRARIES is not required, the Hub has no external library dependency): +/// forge clean && forge script \ +/// scripts/deploy/AaveV4DeployIsolatedHub.s.sol:AaveV4DeployPendlePaxosIsolatedHub \ +/// --rpc-url mainnet --account --slow (--broadcast --verify) +contract AaveV4DeployPendlePaxosIsolatedHub is AaveV4DeployIsolatedHubBase { + uint256 internal constant _ETHEREUM_CHAIN_ID = 1; + + // AaveV4Ethereum.ACCESS_MANAGER + // https://github.com/aave-dao/aave-address-book/blob/c48a741a10b94202f738d52a09e9c9a8bf18a67d/src/AaveV4Ethereum.sol#L8 + address public constant ACCESS_MANAGER = 0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01; + // GovernanceV3Ethereum.EXECUTOR_LVL_1 + // https://github.com/aave-dao/aave-address-book/blob/c48a741a10b94202f738d52a09e9c9a8bf18a67d/src/GovernanceV3Ethereum.sol#L56 + address public constant EXECUTOR_LVL_1 = 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A; + + uint256 internal constant _VERSION = 1; + string internal constant _HUB_LABEL = 'PENDLE_PAXOS_ISOLATED_HUB'; + + function hubSalt(address deployer) public view returns (bytes32) { + bytes32 userSalt = keccak256( + bytes(string.concat('chain ', vm.toString(block.chainid), '_version ', vm.toString(_VERSION))) + ); + bytes32 rootSalt = AaveV4DeployOrchestration._deriveSalt(deployer, userSalt); + return AaveV4DeployOrchestration._deriveChildSalt(rootSalt, 'hub', _HUB_LABEL); + } + + function _getDeployInputs( + address deployer + ) internal view override returns (HubDeployInputs memory) { + return + HubDeployInputs({ + proxyAdminOwner: EXECUTOR_LVL_1, + authority: ACCESS_MANAGER, + salt: hubSalt(deployer) + }); + } + + function _expectedChainId() internal pure override returns (uint256) { + return _ETHEREUM_CHAIN_ID; + } + + function _deploymentName() internal pure override returns (string memory) { + return 'Pendle Paxos Isolated Hub'; + } +} From 4cda98a87e0af5cf901a7630eaa05cae1e5533a3 Mon Sep 17 00:00:00 2001 From: Alexandru Niculae <43644109+avniculae@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:34:10 +0300 Subject: [PATCH 3/6] fix: update import after correlated spoke script rename --- tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol b/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol index 6f3488143..d607fc46f 100644 --- a/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol +++ b/tests/scripts/AaveV4DeployUSDGCorrelatedSpoke.t.sol @@ -10,7 +10,7 @@ import {IPriceOracle} from 'src/spoke/interfaces/IPriceOracle.sol'; import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; -import {AaveV4DeployUSDGCorrelatedSpoke} from 'scripts/deploy/AaveV4DeployUSDGCorrelatedSpoke.s.sol'; +import {AaveV4DeployUSDGCorrelatedSpoke} from 'scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol'; contract AaveV4DeployUSDGCorrelatedSpokeTest is Test { AaveV4DeployUSDGCorrelatedSpoke internal _script; From 0d2f434e679ba8e92430c045d985c283fc325c66 Mon Sep 17 00:00:00 2001 From: Alexandru Niculae <43644109+avniculae@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:25:36 +0300 Subject: [PATCH 4/6] feat: deploy script for Paxos replacement TokenizationSpokes --- .../AaveV4DeployTokenizationSpoke.s.sol | 164 ++++++++++++++ .../PaxosTokenizationSpokesActivation.t.sol | 208 ++++++++++++++++++ .../AaveV4DeployPaxosTokenizationSpokes.t.sol | 119 ++++++++++ 3 files changed, 491 insertions(+) create mode 100644 scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol create mode 100644 tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol create mode 100644 tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol diff --git a/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol b/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol new file mode 100644 index 000000000..97f047c42 --- /dev/null +++ b/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: LicenseRef-BUSL +pragma solidity ^0.8.0; + +import {Script} from 'forge-std/Script.sol'; +import {console2 as console} from 'forge-std/console2.sol'; + +import {AaveV4DeployBase} from 'src/deployments/orchestration/AaveV4DeployBase.sol'; +import {AaveV4DeployOrchestration} from 'src/deployments/orchestration/AaveV4DeployOrchestration.sol'; +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; + +/// @title AaveV4DeployTokenizationSpokeBase +/// @author Aave Labs +/// @notice Generic base script to deploy standalone TokenizationSpoke instances (proxy + implementation) +/// for existing Hubs. Concrete scripts override the deploy inputs, the expected chain id and the +/// deployment name for a specific market. Registration on the Hub (`addSpoke`) is not part of the +/// deployment and is performed separately by governance or the Protocol Security Council. +abstract contract AaveV4DeployTokenizationSpokeBase is Script { + struct TokenizationSpokeDeployInputs { + address hub; + address underlying; + address proxyAdminOwner; + string shareName; + string shareSymbol; + bytes32 salt; + } + + /// @dev Override to provide the market-specific deploy inputs, one entry per TokenizationSpoke. + function _getDeployInputs( + address deployer + ) internal view virtual returns (TokenizationSpokeDeployInputs[] memory); + + /// @dev Override to return the expected chain id for this deployment. + function _expectedChainId() internal view virtual returns (uint256); + + /// @dev Override to return a human-readable name for this deployment (used in logs). + function _deploymentName() internal view virtual returns (string memory); + + function run() + external + virtual + returns (BatchReports.TokenizationSpokeBatchReport[] memory reports) + { + require(block.chainid == _expectedChainId(), 'chain id mismatch'); + + vm.startBroadcast(); + (, address deployer, ) = vm.readCallers(); + TokenizationSpokeDeployInputs[] memory inputs = _getDeployInputs(deployer); + reports = _deploy(inputs); + vm.stopBroadcast(); + + _logReports(deployer, inputs, reports); + } + + function _deploy( + TokenizationSpokeDeployInputs[] memory inputs + ) internal returns (BatchReports.TokenizationSpokeBatchReport[] memory reports) { + reports = new BatchReports.TokenizationSpokeBatchReport[](inputs.length); + for (uint256 i; i < inputs.length; ++i) { + reports[i] = AaveV4DeployBase.deployTokenizationSpokeBatch({ + hub: inputs[i].hub, + underlying: inputs[i].underlying, + proxyAdminOwner: inputs[i].proxyAdminOwner, + shareName: inputs[i].shareName, + shareSymbol: inputs[i].shareSymbol, + salt: inputs[i].salt + }); + } + } + + function _logReports( + address deployer, + TokenizationSpokeDeployInputs[] memory inputs, + BatchReports.TokenizationSpokeBatchReport[] memory reports + ) internal view { + console.log(string.concat(_deploymentName(), ' deployment complete')); + console.log(' deployer :', deployer); + for (uint256 i; i < reports.length; ++i) { + console.log(string.concat(' ', inputs[i].shareSymbol)); + console.log(' hub :', inputs[i].hub); + console.log(' underlying :', inputs[i].underlying); + console.log(' proxyAdminOwner :', inputs[i].proxyAdminOwner); + console.log(' tokenizationSpoke :', reports[i].tokenizationSpokeProxy); + console.log(' tokenizationSpokeImpl:', reports[i].tokenizationSpokeImplementation); + } + } +} + +/// @title AaveV4DeployPaxosTokenizationSpokes +/// @author Aave Labs +/// @notice Deploys replacement TokenizationSpokes (USDC, USDT, PT_USDG_24SEP2026) for the Paxos Hub on +/// Ethereum mainnet. The previously deployed instances are deprecated as their ProxyAdmins are +/// owned by the PayloadsController and can never exercise ownership; the replacements set the +/// ProxyAdmin owner to the Protocol Security Council, matching all other mainnet +/// TokenizationSpokes. Activation on the Hub is performed separately by the Protocol Security +/// Council. +/// @dev Usage: +/// forge clean && forge script \ +/// scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol:AaveV4DeployPaxosTokenizationSpokes \ +/// --rpc-url mainnet --account --slow (--broadcast --verify) +contract AaveV4DeployPaxosTokenizationSpokes is AaveV4DeployTokenizationSpokeBase { + uint256 internal constant _ETHEREUM_CHAIN_ID = 1; + + // AaveV4EthereumHubs.PAXOS_HUB + // https://github.com/aave-dao/aave-address-book/blob/7e444a1e73b538fd0b9e093e5156401d6fccca7d/src/AaveV4Ethereum.sol#L38 + address public constant PAXOS_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; + // Protocol Security Council + // https://etherscan.io/address/0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9 + address public constant PROTOCOL_SECURITY_COUNCIL = 0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9; + + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address public constant PT_USDG_24SEP2026 = 0xc1906aeCf868749a2DeE203F59b904c0cf212140; + + uint256 internal constant _VERSION = 1; + + function tokenizationSpokeSalt( + address deployer, + string memory label + ) public view returns (bytes32) { + bytes32 userSalt = keccak256( + bytes(string.concat('chain ', vm.toString(block.chainid), '_version ', vm.toString(_VERSION))) + ); + bytes32 rootSalt = AaveV4DeployOrchestration._deriveSalt(deployer, userSalt); + return AaveV4DeployOrchestration._deriveChildSalt(rootSalt, 'tokenization-spoke', label); + } + + function _getDeployInputs( + address deployer + ) internal view override returns (TokenizationSpokeDeployInputs[] memory inputs) { + inputs = new TokenizationSpokeDeployInputs[](3); + inputs[0] = TokenizationSpokeDeployInputs({ + hub: PAXOS_HUB, + underlying: USDC, + proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, + shareName: 'Wrapped Aave Paxos USDC', + shareSymbol: 'waPaxosUSDC', + salt: tokenizationSpokeSalt(deployer, 'waPaxosUSDC') + }); + inputs[1] = TokenizationSpokeDeployInputs({ + hub: PAXOS_HUB, + underlying: USDT, + proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, + shareName: 'Wrapped Aave Paxos USDT', + shareSymbol: 'waPaxosUSDT', + salt: tokenizationSpokeSalt(deployer, 'waPaxosUSDT') + }); + inputs[2] = TokenizationSpokeDeployInputs({ + hub: PAXOS_HUB, + underlying: PT_USDG_24SEP2026, + proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, + shareName: 'Wrapped Aave Paxos PT_USDG_24SEP2026', + shareSymbol: 'waPaxosPT_USDG_24SEP2026', + salt: tokenizationSpokeSalt(deployer, 'waPaxosPT_USDG_24SEP2026') + }); + } + + function _expectedChainId() internal pure override returns (uint256) { + return _ETHEREUM_CHAIN_ID; + } + + function _deploymentName() internal pure override returns (string memory) { + return 'Paxos TokenizationSpokes'; + } +} diff --git a/tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol b/tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol new file mode 100644 index 000000000..12b3f12ce --- /dev/null +++ b/tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {IERC20} from 'src/dependencies/openzeppelin/IERC20.sol'; +import {SafeERC20} from 'src/dependencies/openzeppelin/SafeERC20.sol'; +import {IHub} from 'src/hub/interfaces/IHub.sol'; +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; + +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; + +/// @dev Validates the Paxos TokenizationSpoke replacement on a devnet where the Security Council +/// activation batch (`output/paxos-tokenization-spokes-activation.json`) has been executed. +/// Skipped unless TENDERLY_DEVNET_RPC is set. +contract PaxosTokenizationSpokesActivationTest is Test { + using SafeERC20 for IERC20; + + address internal constant PAXOS_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; + address internal constant PROTOCOL_SECURITY_COUNCIL = 0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9; + address internal constant PAYLOADS_CONTROLLER = 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5; + address internal constant EXECUTOR_LVL_1 = 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A; + + address internal constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address internal constant PT_USDG_24SEP2026 = 0xc1906aeCf868749a2DeE203F59b904c0cf212140; + + uint256 internal constant PT_USDG_ASSET_ID = 0; + uint256 internal constant USDC_ASSET_ID = 1; + uint256 internal constant USDT_ASSET_ID = 2; + + address internal constant NEW_WA_PAXOS_USDC = 0xFaB44fbD00C5056956BC1c4d681A80563E10d2fD; + address internal constant NEW_WA_PAXOS_USDT = 0xF38C21AE3b87981e954c4eF6b5C1Cbd4BfB00E27; + address internal constant NEW_WA_PAXOS_PT_USDG = 0xB4086ae520EA1314b3EE7f899887acfD5ccdE406; + + address internal constant OLD_WA_PAXOS_USDC = 0x4131E0B2E7AFeCEAf3d3b4225aA61a3B2B7535b8; + address internal constant OLD_WA_PAXOS_USDT = 0x8Dabe53E8cB991c57f0307F6f419E6D469b0deAA; + address internal constant OLD_WA_PAXOS_PT_USDG = 0x27eF1140364948A0E30E248297FfDFE5a4091ec4; + + address internal constant OLD_WA_PAXOS_USDC_HOLDER = 0x9cCf93089cb14F94BAeB8822F8CeFfd91Bd71649; + + uint40 internal constant ADD_CAP = 13_000_000; + + address internal USER = makeAddr('USER'); + + bool internal _devnetAvailable; + + modifier onlyDevnet() { + vm.skip(!_devnetAvailable, 'TENDERLY_DEVNET_RPC not set'); + _; + } + + function setUp() public { + string memory rpc = vm.envOr('TENDERLY_DEVNET_RPC', string('')); + if (bytes(rpc).length == 0) return; + vm.createSelectFork(rpc); + _devnetAvailable = true; + } + + function test_newSpokes_proxyAdminOwnership() public onlyDevnet { + address[3] memory spokes = [NEW_WA_PAXOS_USDC, NEW_WA_PAXOS_USDT, NEW_WA_PAXOS_PT_USDG]; + for (uint256 i; i < spokes.length; ++i) { + address owner = Ownable(ProxyHelper.getProxyAdmin(spokes[i])).owner(); + assertEq(owner, PROTOCOL_SECURITY_COUNCIL); + assertNotEq(owner, PAYLOADS_CONTROLLER); + assertNotEq(owner, EXECUTOR_LVL_1); + } + } + + function test_newSpokes_activationState() public onlyDevnet { + _assertSpokeConfig({ + assetId: USDC_ASSET_ID, + spoke: NEW_WA_PAXOS_USDC, + underlying: USDC, + expectedAddCap: ADD_CAP + }); + _assertSpokeConfig({ + assetId: USDT_ASSET_ID, + spoke: NEW_WA_PAXOS_USDT, + underlying: USDT, + expectedAddCap: ADD_CAP + }); + _assertSpokeConfig({ + assetId: PT_USDG_ASSET_ID, + spoke: NEW_WA_PAXOS_PT_USDG, + underlying: PT_USDG_24SEP2026, + expectedAddCap: 0 + }); + } + + function test_oldSpokes_remainFrozen() public onlyDevnet { + _assertSpokeConfig({ + assetId: USDC_ASSET_ID, + spoke: OLD_WA_PAXOS_USDC, + underlying: USDC, + expectedAddCap: 0 + }); + _assertSpokeConfig({ + assetId: USDT_ASSET_ID, + spoke: OLD_WA_PAXOS_USDT, + underlying: USDT, + expectedAddCap: 0 + }); + _assertSpokeConfig({ + assetId: PT_USDG_ASSET_ID, + spoke: OLD_WA_PAXOS_PT_USDG, + underlying: PT_USDG_24SEP2026, + expectedAddCap: 0 + }); + } + + function test_newUsdcSpoke_depositAndRedeem() public onlyDevnet { + _depositAndRedeem(NEW_WA_PAXOS_USDC, USDC, 1000e6); + } + + function test_newUsdtSpoke_depositAndRedeem() public onlyDevnet { + _depositAndRedeem(NEW_WA_PAXOS_USDT, USDT, 1000e6); + } + + function test_newUsdcSpoke_depositAboveCapReverts() public onlyDevnet { + uint256 amount = (uint256(ADD_CAP) + 1) * 1e6; + deal(USDC, USER, amount); + vm.startPrank(USER); + IERC20(USDC).forceApprove(NEW_WA_PAXOS_USDC, amount); + vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, ADD_CAP)); + ITokenizationSpoke(NEW_WA_PAXOS_USDC).deposit(amount, USER); + vm.stopPrank(); + } + + function test_newPtSpoke_depositReverts_zeroCap() public onlyDevnet { + deal(PT_USDG_24SEP2026, USER, 100e6); + vm.startPrank(USER); + IERC20(PT_USDG_24SEP2026).forceApprove(NEW_WA_PAXOS_PT_USDG, 100e6); + vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, 0)); + ITokenizationSpoke(NEW_WA_PAXOS_PT_USDG).deposit(100e6, USER); + vm.stopPrank(); + } + + function test_oldSpokes_depositsBlocked() public onlyDevnet { + deal(USDC, USER, 100e6); + deal(USDT, USER, 100e6); + vm.startPrank(USER); + + IERC20(USDC).forceApprove(OLD_WA_PAXOS_USDC, 100e6); + vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, 0)); + ITokenizationSpoke(OLD_WA_PAXOS_USDC).deposit(100e6, USER); + + IERC20(USDT).forceApprove(OLD_WA_PAXOS_USDT, 100e6); + vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, 0)); + ITokenizationSpoke(OLD_WA_PAXOS_USDT).deposit(100e6, USER); + + vm.stopPrank(); + } + + function test_oldUsdcSpoke_withdrawalsOpen() public onlyDevnet { + ITokenizationSpoke oldSpoke = ITokenizationSpoke(OLD_WA_PAXOS_USDC); + uint256 shares = oldSpoke.balanceOf(OLD_WA_PAXOS_USDC_HOLDER); + assertGt(shares, 0); + + uint256 balanceBefore = IERC20(USDC).balanceOf(OLD_WA_PAXOS_USDC_HOLDER); + vm.prank(OLD_WA_PAXOS_USDC_HOLDER); + uint256 assets = oldSpoke.redeem(shares, OLD_WA_PAXOS_USDC_HOLDER, OLD_WA_PAXOS_USDC_HOLDER); + + assertGt(assets, 0); + assertEq( + IERC20(USDC).balanceOf(OLD_WA_PAXOS_USDC_HOLDER), + balanceBefore + assets, + 'holder should be able to fully exit the frozen spoke' + ); + assertEq(oldSpoke.balanceOf(OLD_WA_PAXOS_USDC_HOLDER), 0); + } + + function _depositAndRedeem(address spoke, address underlying, uint256 amount) internal { + deal(underlying, USER, amount); + vm.startPrank(USER); + IERC20(underlying).forceApprove(spoke, amount); + + uint256 shares = ITokenizationSpoke(spoke).deposit(amount, USER); + assertGt(shares, 0); + assertEq(ITokenizationSpoke(spoke).balanceOf(USER), shares); + + uint256 assets = ITokenizationSpoke(spoke).redeem(shares, USER, USER); + vm.stopPrank(); + + assertEq(ITokenizationSpoke(spoke).balanceOf(USER), 0); + assertApproxEqAbs(assets, amount, 2, 'redeem should return the deposited amount'); + assertEq(IERC20(underlying).balanceOf(USER), assets); + } + + function _assertSpokeConfig( + uint256 assetId, + address spoke, + address underlying, + uint40 expectedAddCap + ) internal view { + IHub hub = IHub(PAXOS_HUB); + assertTrue(hub.isSpokeListed(assetId, spoke)); + assertEq(ITokenizationSpoke(spoke).asset(), underlying); + + IHub.SpokeConfig memory config = hub.getSpokeConfig(assetId, spoke); + assertEq(config.addCap, expectedAddCap); + assertEq(config.drawCap, 0); + assertEq(config.riskPremiumThreshold, 0); + assertTrue(config.active); + assertFalse(config.halted); + } +} diff --git a/tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol b/tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol new file mode 100644 index 000000000..8df4ff3cb --- /dev/null +++ b/tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; + +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; +import {AaveV4DeployPaxosTokenizationSpokes} from 'scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol'; + +contract AaveV4DeployPaxosTokenizationSpokesTest is Test { + // deprecated instances whose ProxyAdmins are owned by the PayloadsController + address internal constant DEPRECATED_WA_PAXOS_USDC = 0x4131E0B2E7AFeCEAf3d3b4225aA61a3B2B7535b8; + address internal constant DEPRECATED_WA_PAXOS_USDT = 0x8Dabe53E8cB991c57f0307F6f419E6D469b0deAA; + address internal constant DEPRECATED_WA_PAXOS_PT_USDG = + 0x27eF1140364948A0E30E248297FfDFE5a4091ec4; + // GovernanceV3Ethereum.PAYLOADS_CONTROLLER + address internal constant PAYLOADS_CONTROLLER = 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5; + // GovernanceV3Ethereum.EXECUTOR_LVL_1 + address internal constant EXECUTOR_LVL_1 = 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A; + // AaveV4EthereumTokenizationSpokes.CORE_USDC_TOKENIZATION_SPOKE + address internal constant CORE_USDC_TOKENIZATION_SPOKE = + 0x531E90a2376902DE8915789Fcc1075e3B0c153E7; + + AaveV4DeployPaxosTokenizationSpokes internal _script; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 25544900); + _script = new AaveV4DeployPaxosTokenizationSpokes(); + } + + function test_run_deploysTokenizationSpokes() public { + BatchReports.TokenizationSpokeBatchReport[] memory reports = _script.run(); + assertEq(reports.length, 3); + + address[3] memory underlyings = [_script.USDC(), _script.USDT(), _script.PT_USDG_24SEP2026()]; + string[3] memory names = [ + 'Wrapped Aave Paxos USDC', + 'Wrapped Aave Paxos USDT', + 'Wrapped Aave Paxos PT_USDG_24SEP2026' + ]; + string[3] memory symbols = ['waPaxosUSDC', 'waPaxosUSDT', 'waPaxosPT_USDG_24SEP2026']; + address[3] memory deprecated = [ + DEPRECATED_WA_PAXOS_USDC, + DEPRECATED_WA_PAXOS_USDT, + DEPRECATED_WA_PAXOS_PT_USDG + ]; + + for (uint256 i; i < reports.length; ++i) { + address proxy = reports[i].tokenizationSpokeProxy; + assertGt(proxy.code.length, 0); + assertGt(reports[i].tokenizationSpokeImplementation.code.length, 0); + assertNotEq(proxy, deprecated[i]); + + assertEq(ITokenizationSpoke(proxy).hub(), _script.PAXOS_HUB()); + assertEq(ITokenizationSpoke(proxy).asset(), underlyings[i]); + assertEq(ITokenizationSpoke(proxy).name(), names[i]); + assertEq(ITokenizationSpoke(proxy).symbol(), symbols[i]); + + address proxyAdminOwner = Ownable(ProxyHelper.getProxyAdmin(proxy)).owner(); + assertEq( + proxyAdminOwner, + _script.PROTOCOL_SECURITY_COUNCIL(), + 'ProxyAdmin owner should be the Protocol Security Council' + ); + assertNotEq( + proxyAdminOwner, + PAYLOADS_CONTROLLER, + 'ProxyAdmin owner must never be the PayloadsController' + ); + assertNotEq( + proxyAdminOwner, + EXECUTOR_LVL_1, + 'ProxyAdmin owner should not be the DAO executor' + ); + } + } + + function test_run_revertsOffMainnet_fuzz(uint64 wrongChainId) public { + vm.assume(wrongChainId != 1); + vm.chainId(wrongChainId); + + vm.expectRevert('chain id mismatch'); + _script.run(); + } + + function test_constantsMatchOnchainState() public view { + assertEq(_script.PAXOS_HUB(), 0x62d63197660c080236193CA60b70E49A08E90368); + + // the intended owner is the owner of the healthy mainnet TokenizationSpoke ProxyAdmins + assertEq( + _script.PROTOCOL_SECURITY_COUNCIL(), + Ownable(ProxyHelper.getProxyAdmin(CORE_USDC_TOKENIZATION_SPOKE)).owner() + ); + + // deploy inputs must match the deprecated instances they replace + assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDC).asset(), _script.USDC()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDT).asset(), _script.USDT()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_PT_USDG).asset(), _script.PT_USDG_24SEP2026()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDC).hub(), _script.PAXOS_HUB()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDT).hub(), _script.PAXOS_HUB()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_PT_USDG).hub(), _script.PAXOS_HUB()); + } + + function test_tokenizationSpokeSaltMatchesOrchestrationFormula_fuzz( + address deployer + ) public view { + bytes32 orchestrationSalt = keccak256('AAVE_V4'); + bytes32 userSalt = keccak256(bytes('chain 1_version 1')); + bytes32 expectedRoot = bytes32(bytes20(deployer)) | + (keccak256(abi.encode(orchestrationSalt, userSalt)) >> 160); + bytes32 expected = keccak256(abi.encode(expectedRoot, 'tokenization-spoke', 'waPaxosUSDC')); + + assertEq(_script.tokenizationSpokeSalt(deployer, 'waPaxosUSDC'), expected); + } +} From ba9d979751a6e3bc61874a7299559d456ab70b47 Mon Sep 17 00:00:00 2001 From: Kogaroshi <25688223+Kogaroshi@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:45:41 +0100 Subject: [PATCH 5/6] feat: deploy Maple Spoke --- .../deploy/AaveV4DeployCorrelatedSpoke.s.sol | 52 ++++++++++++ .../AaveV4DeployMapleCorrelatedSpoke.t.sol | 85 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 tests/scripts/AaveV4DeployMapleCorrelatedSpoke.t.sol diff --git a/scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol b/scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol index 80fa712a6..cd73030a1 100644 --- a/scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol +++ b/scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol @@ -129,3 +129,55 @@ contract AaveV4DeployUSDGCorrelatedSpoke is AaveV4DeployCorrelatedSpokeBase { return 'USDG Correlated Spoke'; } } + +/// @title AaveV4DeployMapleCorrelatedSpoke +/// @author Aave Labs +/// @notice Deploys the Maple correlated-asset Spoke (syrupUSDG collateral against USDG) on the Global +/// Dollar Hub, Ethereum mainnet. Reserve, price source, cap and interest rate configuration are +/// performed separately by the Protocol Security Council after deployment. +/// @dev Usage (make sure FOUNDRY_LIBRARIES is populated in .env with the LiquidationLogic address): +/// forge clean && forge script \ +/// scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol:AaveV4DeployMapleCorrelatedSpoke \ +/// --rpc-url mainnet --account --slow (--broadcast --verify) +contract AaveV4DeployMapleCorrelatedSpoke is AaveV4DeployCorrelatedSpokeBase { + uint256 internal constant _ETHEREUM_CHAIN_ID = 1; + + // AaveV4Ethereum.ACCESS_MANAGER + // https://github.com/aave-dao/aave-address-book/blob/c48a741a10b94202f738d52a09e9c9a8bf18a67d/src/AaveV4Ethereum.sol#L8 + address public constant ACCESS_MANAGER = 0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01; + // Protocol Security Council + // https://etherscan.io/address/0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9 + address public constant PROTOCOL_SECURITY_COUNCIL = 0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9; + + uint256 internal constant _VERSION = 1; + string internal constant _SPOKE_LABEL = 'MAPLE_CORRELATED_SPOKE'; + + function spokeSalt(address deployer) public view returns (bytes32) { + bytes32 userSalt = keccak256( + bytes(string.concat('chain ', vm.toString(block.chainid), '_version ', vm.toString(_VERSION))) + ); + bytes32 rootSalt = AaveV4DeployOrchestration._deriveSalt(deployer, userSalt); + return AaveV4DeployOrchestration._deriveChildSalt(rootSalt, 'spoke', _SPOKE_LABEL); + } + + function _getDeployInputs( + address deployer + ) internal view override returns (SpokeDeployInputs memory) { + return + SpokeDeployInputs({ + proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, + authority: ACCESS_MANAGER, + oracleDecimals: DeployConstants.ORACLE_DECIMALS, + maxUserReservesLimit: DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT, + salt: spokeSalt(deployer) + }); + } + + function _expectedChainId() internal pure override returns (uint256) { + return _ETHEREUM_CHAIN_ID; + } + + function _deploymentName() internal pure override returns (string memory) { + return 'Maple Correlated Spoke'; + } +} diff --git a/tests/scripts/AaveV4DeployMapleCorrelatedSpoke.t.sol b/tests/scripts/AaveV4DeployMapleCorrelatedSpoke.t.sol new file mode 100644 index 000000000..b58539338 --- /dev/null +++ b/tests/scripts/AaveV4DeployMapleCorrelatedSpoke.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from 'forge-std/Test.sol'; + +import {IAccessManaged} from 'src/dependencies/openzeppelin/IAccessManaged.sol'; +import {Ownable} from 'src/dependencies/openzeppelin/Ownable.sol'; +import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol'; +import {IPriceOracle} from 'src/spoke/interfaces/IPriceOracle.sol'; + +import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; +import {DeployConstants} from 'src/deployments/utils/libraries/DeployConstants.sol'; + +import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; + +import {AaveV4DeployMapleCorrelatedSpoke} from 'scripts/deploy/AaveV4DeployCorrelatedSpoke.s.sol'; + +contract AaveV4DeployMapleCorrelatedSpokeTest is Test { + AaveV4DeployMapleCorrelatedSpoke internal _script; + + function setUp() public { + vm.createSelectFork(vm.rpcUrl('mainnet'), 25092080); + _script = new AaveV4DeployMapleCorrelatedSpoke(); + } + + function test_run_deploysSpoke() public { + BatchReports.SpokeInstanceBatchReport memory report = _script.run(); + + assertGt(report.spokeProxy.code.length, 0); + assertGt(report.spokeImplementation.code.length, 0); + assertGt(report.aaveOracle.code.length, 0); + + assertEq(IAccessManaged(report.spokeProxy).authority(), _script.ACCESS_MANAGER()); + assertEq(ISpoke(report.spokeProxy).ORACLE(), report.aaveOracle); + assertEq(IPriceOracle(report.aaveOracle).spoke(), report.spokeProxy); + assertEq( + uint256(IPriceOracle(report.aaveOracle).decimals()), + uint256(DeployConstants.ORACLE_DECIMALS) + ); + assertEq( + uint256(ISpoke(report.spokeProxy).MAX_USER_RESERVES_LIMIT()), + uint256(DeployConstants.MAX_ALLOWED_USER_RESERVES_LIMIT) + ); + } + + function test_run_proxyAdminOwnedBySecurityCouncil() public { + BatchReports.SpokeInstanceBatchReport memory report = _script.run(); + + address owner = Ownable(ProxyHelper.getProxyAdmin(report.spokeProxy)).owner(); + assertEq(owner, _script.PROTOCOL_SECURITY_COUNCIL()); + } + + // Same salt does NOT collide: each batch deploys a fresh CREATE-allocated AaveOracle whose + // address is in SpokeInstance's init code, so the CREATE2 spoke address differs across calls. + // Operator must avoid running the script twice — no on-chain safety check. + function test_run_repeatCallsProduceDistinctSpokes() public { + BatchReports.SpokeInstanceBatchReport memory a = _script.run(); + BatchReports.SpokeInstanceBatchReport memory b = _script.run(); + assertNotEq(a.spokeProxy, b.spokeProxy); + assertNotEq(a.aaveOracle, b.aaveOracle); + } + + function test_run_revertsOffMainnet_fuzz(uint64 wrongChainId) public { + vm.assume(wrongChainId != 1); + vm.chainId(wrongChainId); + + vm.expectRevert('chain id mismatch'); + _script.run(); + } + + function test_constantsMatchAddressBook() public view { + assertEq(_script.ACCESS_MANAGER(), 0x08aE3BE30958cDd1847ec58fFfd4C451a87fDF01); + assertEq(_script.PROTOCOL_SECURITY_COUNCIL(), 0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9); + } + + function test_spokeSaltMatchesOrchestrationFormula_fuzz(address deployer) public view { + bytes32 orchestrationSalt = keccak256('AAVE_V4'); + bytes32 userSalt = keccak256(bytes('chain 1_version 1')); + bytes32 expectedRoot = bytes32(bytes20(deployer)) | + (keccak256(abi.encode(orchestrationSalt, userSalt)) >> 160); + bytes32 expected = keccak256(abi.encode(expectedRoot, 'spoke', 'MAPLE_CORRELATED_SPOKE')); + + assertEq(_script.spokeSalt(deployer), expected); + } +} From 6586a871f049a6bce7067166d8437e12c7545e7b Mon Sep 17 00:00:00 2001 From: Alexandru Niculae <43644109+avniculae@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:55:40 +0300 Subject: [PATCH 6/6] fix: rename Paxos to Global Dollar in TokenizationSpokes deploy script The Paxos Hub was renamed to Global Dollar Hub, so the replacement TokenizationSpokes take the Global Dollar naming: shareName `Wrapped Aave Global Dollar ` and shareSymbol `waGlobalDollar`. Both feed the CREATE2 address (the symbol is the salt label and both go into the proxy init data), so the three instances already deployed on mainnet (0xFaB4..d2fD, 0xF38C..0E27, 0xB408..E406) are obsolete and need a redeploy. The activation batch has not been executed on mainnet, so nothing is live. The `NEW_WA_GLOBAL_DOLLAR_*` addresses in the activation fork test are the deterministic addresses for deployer 0x0315d353045f8FBCDd8CAcEbA40b019d094B670E; the same derivation reproduces the three currently deployed addresses exactly. A new devnet is required for that test to pass again. --- .../AaveV4DeployTokenizationSpoke.s.sol | 40 ++++----- ...lDollarTokenizationSpokesActivation.t.sol} | 83 +++++++++++-------- ...eployGlobalDollarTokenizationSpokes.t.sol} | 68 +++++++++------ 3 files changed, 111 insertions(+), 80 deletions(-) rename tests/deployments/fork/{PaxosTokenizationSpokesActivation.t.sol => GlobalDollarTokenizationSpokesActivation.t.sol} (65%) rename tests/scripts/{AaveV4DeployPaxosTokenizationSpokes.t.sol => AaveV4DeployGlobalDollarTokenizationSpokes.t.sol} (62%) diff --git a/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol b/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol index 97f047c42..2039b9c14 100644 --- a/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol +++ b/scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol @@ -85,24 +85,24 @@ abstract contract AaveV4DeployTokenizationSpokeBase is Script { } } -/// @title AaveV4DeployPaxosTokenizationSpokes +/// @title AaveV4DeployGlobalDollarTokenizationSpokes /// @author Aave Labs -/// @notice Deploys replacement TokenizationSpokes (USDC, USDT, PT_USDG_24SEP2026) for the Paxos Hub on -/// Ethereum mainnet. The previously deployed instances are deprecated as their ProxyAdmins are -/// owned by the PayloadsController and can never exercise ownership; the replacements set the +/// @notice Deploys replacement TokenizationSpokes (USDC, USDT, PT_USDG_24SEP2026) for the Global Dollar +/// Hub on Ethereum mainnet. The previously deployed instances are deprecated as their ProxyAdmins +/// are owned by the PayloadsController and can never exercise ownership; the replacements set the /// ProxyAdmin owner to the Protocol Security Council, matching all other mainnet /// TokenizationSpokes. Activation on the Hub is performed separately by the Protocol Security /// Council. /// @dev Usage: /// forge clean && forge script \ -/// scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol:AaveV4DeployPaxosTokenizationSpokes \ +/// scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol:AaveV4DeployGlobalDollarTokenizationSpokes \ /// --rpc-url mainnet --account --slow (--broadcast --verify) -contract AaveV4DeployPaxosTokenizationSpokes is AaveV4DeployTokenizationSpokeBase { +contract AaveV4DeployGlobalDollarTokenizationSpokes is AaveV4DeployTokenizationSpokeBase { uint256 internal constant _ETHEREUM_CHAIN_ID = 1; // AaveV4EthereumHubs.PAXOS_HUB // https://github.com/aave-dao/aave-address-book/blob/7e444a1e73b538fd0b9e093e5156401d6fccca7d/src/AaveV4Ethereum.sol#L38 - address public constant PAXOS_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; + address public constant GLOBAL_DOLLAR_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; // Protocol Security Council // https://etherscan.io/address/0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9 address public constant PROTOCOL_SECURITY_COUNCIL = 0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9; @@ -129,28 +129,28 @@ contract AaveV4DeployPaxosTokenizationSpokes is AaveV4DeployTokenizationSpokeBas ) internal view override returns (TokenizationSpokeDeployInputs[] memory inputs) { inputs = new TokenizationSpokeDeployInputs[](3); inputs[0] = TokenizationSpokeDeployInputs({ - hub: PAXOS_HUB, + hub: GLOBAL_DOLLAR_HUB, underlying: USDC, proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, - shareName: 'Wrapped Aave Paxos USDC', - shareSymbol: 'waPaxosUSDC', - salt: tokenizationSpokeSalt(deployer, 'waPaxosUSDC') + shareName: 'Wrapped Aave Global Dollar USDC', + shareSymbol: 'waGlobalDollarUSDC', + salt: tokenizationSpokeSalt(deployer, 'waGlobalDollarUSDC') }); inputs[1] = TokenizationSpokeDeployInputs({ - hub: PAXOS_HUB, + hub: GLOBAL_DOLLAR_HUB, underlying: USDT, proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, - shareName: 'Wrapped Aave Paxos USDT', - shareSymbol: 'waPaxosUSDT', - salt: tokenizationSpokeSalt(deployer, 'waPaxosUSDT') + shareName: 'Wrapped Aave Global Dollar USDT', + shareSymbol: 'waGlobalDollarUSDT', + salt: tokenizationSpokeSalt(deployer, 'waGlobalDollarUSDT') }); inputs[2] = TokenizationSpokeDeployInputs({ - hub: PAXOS_HUB, + hub: GLOBAL_DOLLAR_HUB, underlying: PT_USDG_24SEP2026, proxyAdminOwner: PROTOCOL_SECURITY_COUNCIL, - shareName: 'Wrapped Aave Paxos PT_USDG_24SEP2026', - shareSymbol: 'waPaxosPT_USDG_24SEP2026', - salt: tokenizationSpokeSalt(deployer, 'waPaxosPT_USDG_24SEP2026') + shareName: 'Wrapped Aave Global Dollar PT_USDG_24SEP2026', + shareSymbol: 'waGlobalDollarPT_USDG_24SEP2026', + salt: tokenizationSpokeSalt(deployer, 'waGlobalDollarPT_USDG_24SEP2026') }); } @@ -159,6 +159,6 @@ contract AaveV4DeployPaxosTokenizationSpokes is AaveV4DeployTokenizationSpokeBas } function _deploymentName() internal pure override returns (string memory) { - return 'Paxos TokenizationSpokes'; + return 'Global Dollar TokenizationSpokes'; } } diff --git a/tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol b/tests/deployments/fork/GlobalDollarTokenizationSpokesActivation.t.sol similarity index 65% rename from tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol rename to tests/deployments/fork/GlobalDollarTokenizationSpokesActivation.t.sol index 12b3f12ce..f7c721b64 100644 --- a/tests/deployments/fork/PaxosTokenizationSpokesActivation.t.sol +++ b/tests/deployments/fork/GlobalDollarTokenizationSpokesActivation.t.sol @@ -11,13 +11,13 @@ import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; -/// @dev Validates the Paxos TokenizationSpoke replacement on a devnet where the Security Council -/// activation batch (`output/paxos-tokenization-spokes-activation.json`) has been executed. +/// @dev Validates the Global Dollar TokenizationSpoke replacement on a devnet where the Security Council +/// activation batch (`output/global-dollar-tokenization-spokes-activation.json`) has been executed. /// Skipped unless TENDERLY_DEVNET_RPC is set. -contract PaxosTokenizationSpokesActivationTest is Test { +contract GlobalDollarTokenizationSpokesActivationTest is Test { using SafeERC20 for IERC20; - address internal constant PAXOS_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; + address internal constant GLOBAL_DOLLAR_HUB = 0x62d63197660c080236193CA60b70E49A08E90368; address internal constant PROTOCOL_SECURITY_COUNCIL = 0x187AAE17d4931310B3fc75743e7F16Bdc9eD77e9; address internal constant PAYLOADS_CONTROLLER = 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5; address internal constant EXECUTOR_LVL_1 = 0x5300A1a15135EA4dc7aD5a167152C01EFc9b192A; @@ -30,15 +30,18 @@ contract PaxosTokenizationSpokesActivationTest is Test { uint256 internal constant USDC_ASSET_ID = 1; uint256 internal constant USDT_ASSET_ID = 2; - address internal constant NEW_WA_PAXOS_USDC = 0xFaB44fbD00C5056956BC1c4d681A80563E10d2fD; - address internal constant NEW_WA_PAXOS_USDT = 0xF38C21AE3b87981e954c4eF6b5C1Cbd4BfB00E27; - address internal constant NEW_WA_PAXOS_PT_USDG = 0xB4086ae520EA1314b3EE7f899887acfD5ccdE406; + address internal constant NEW_WA_GLOBAL_DOLLAR_USDC = 0xaed7c529bD2878170B61C758DfAa215AC7a4FD07; + address internal constant NEW_WA_GLOBAL_DOLLAR_USDT = 0xa0e97e45C2f89003730E467Bd484fA3eEcE5B4Cf; + address internal constant NEW_WA_GLOBAL_DOLLAR_PT_USDG = + 0x7Df10B4A01350D2A1d95cFbE7c9207d7210A2663; - address internal constant OLD_WA_PAXOS_USDC = 0x4131E0B2E7AFeCEAf3d3b4225aA61a3B2B7535b8; - address internal constant OLD_WA_PAXOS_USDT = 0x8Dabe53E8cB991c57f0307F6f419E6D469b0deAA; - address internal constant OLD_WA_PAXOS_PT_USDG = 0x27eF1140364948A0E30E248297FfDFE5a4091ec4; + address internal constant OLD_WA_GLOBAL_DOLLAR_USDC = 0x4131E0B2E7AFeCEAf3d3b4225aA61a3B2B7535b8; + address internal constant OLD_WA_GLOBAL_DOLLAR_USDT = 0x8Dabe53E8cB991c57f0307F6f419E6D469b0deAA; + address internal constant OLD_WA_GLOBAL_DOLLAR_PT_USDG = + 0x27eF1140364948A0E30E248297FfDFE5a4091ec4; - address internal constant OLD_WA_PAXOS_USDC_HOLDER = 0x9cCf93089cb14F94BAeB8822F8CeFfd91Bd71649; + address internal constant OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER = + 0x9cCf93089cb14F94BAeB8822F8CeFfd91Bd71649; uint40 internal constant ADD_CAP = 13_000_000; @@ -59,7 +62,11 @@ contract PaxosTokenizationSpokesActivationTest is Test { } function test_newSpokes_proxyAdminOwnership() public onlyDevnet { - address[3] memory spokes = [NEW_WA_PAXOS_USDC, NEW_WA_PAXOS_USDT, NEW_WA_PAXOS_PT_USDG]; + address[3] memory spokes = [ + NEW_WA_GLOBAL_DOLLAR_USDC, + NEW_WA_GLOBAL_DOLLAR_USDT, + NEW_WA_GLOBAL_DOLLAR_PT_USDG + ]; for (uint256 i; i < spokes.length; ++i) { address owner = Ownable(ProxyHelper.getProxyAdmin(spokes[i])).owner(); assertEq(owner, PROTOCOL_SECURITY_COUNCIL); @@ -71,19 +78,19 @@ contract PaxosTokenizationSpokesActivationTest is Test { function test_newSpokes_activationState() public onlyDevnet { _assertSpokeConfig({ assetId: USDC_ASSET_ID, - spoke: NEW_WA_PAXOS_USDC, + spoke: NEW_WA_GLOBAL_DOLLAR_USDC, underlying: USDC, expectedAddCap: ADD_CAP }); _assertSpokeConfig({ assetId: USDT_ASSET_ID, - spoke: NEW_WA_PAXOS_USDT, + spoke: NEW_WA_GLOBAL_DOLLAR_USDT, underlying: USDT, expectedAddCap: ADD_CAP }); _assertSpokeConfig({ assetId: PT_USDG_ASSET_ID, - spoke: NEW_WA_PAXOS_PT_USDG, + spoke: NEW_WA_GLOBAL_DOLLAR_PT_USDG, underlying: PT_USDG_24SEP2026, expectedAddCap: 0 }); @@ -92,48 +99,48 @@ contract PaxosTokenizationSpokesActivationTest is Test { function test_oldSpokes_remainFrozen() public onlyDevnet { _assertSpokeConfig({ assetId: USDC_ASSET_ID, - spoke: OLD_WA_PAXOS_USDC, + spoke: OLD_WA_GLOBAL_DOLLAR_USDC, underlying: USDC, expectedAddCap: 0 }); _assertSpokeConfig({ assetId: USDT_ASSET_ID, - spoke: OLD_WA_PAXOS_USDT, + spoke: OLD_WA_GLOBAL_DOLLAR_USDT, underlying: USDT, expectedAddCap: 0 }); _assertSpokeConfig({ assetId: PT_USDG_ASSET_ID, - spoke: OLD_WA_PAXOS_PT_USDG, + spoke: OLD_WA_GLOBAL_DOLLAR_PT_USDG, underlying: PT_USDG_24SEP2026, expectedAddCap: 0 }); } function test_newUsdcSpoke_depositAndRedeem() public onlyDevnet { - _depositAndRedeem(NEW_WA_PAXOS_USDC, USDC, 1000e6); + _depositAndRedeem(NEW_WA_GLOBAL_DOLLAR_USDC, USDC, 1000e6); } function test_newUsdtSpoke_depositAndRedeem() public onlyDevnet { - _depositAndRedeem(NEW_WA_PAXOS_USDT, USDT, 1000e6); + _depositAndRedeem(NEW_WA_GLOBAL_DOLLAR_USDT, USDT, 1000e6); } function test_newUsdcSpoke_depositAboveCapReverts() public onlyDevnet { uint256 amount = (uint256(ADD_CAP) + 1) * 1e6; deal(USDC, USER, amount); vm.startPrank(USER); - IERC20(USDC).forceApprove(NEW_WA_PAXOS_USDC, amount); + IERC20(USDC).forceApprove(NEW_WA_GLOBAL_DOLLAR_USDC, amount); vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, ADD_CAP)); - ITokenizationSpoke(NEW_WA_PAXOS_USDC).deposit(amount, USER); + ITokenizationSpoke(NEW_WA_GLOBAL_DOLLAR_USDC).deposit(amount, USER); vm.stopPrank(); } function test_newPtSpoke_depositReverts_zeroCap() public onlyDevnet { deal(PT_USDG_24SEP2026, USER, 100e6); vm.startPrank(USER); - IERC20(PT_USDG_24SEP2026).forceApprove(NEW_WA_PAXOS_PT_USDG, 100e6); + IERC20(PT_USDG_24SEP2026).forceApprove(NEW_WA_GLOBAL_DOLLAR_PT_USDG, 100e6); vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, 0)); - ITokenizationSpoke(NEW_WA_PAXOS_PT_USDG).deposit(100e6, USER); + ITokenizationSpoke(NEW_WA_GLOBAL_DOLLAR_PT_USDG).deposit(100e6, USER); vm.stopPrank(); } @@ -142,33 +149,37 @@ contract PaxosTokenizationSpokesActivationTest is Test { deal(USDT, USER, 100e6); vm.startPrank(USER); - IERC20(USDC).forceApprove(OLD_WA_PAXOS_USDC, 100e6); + IERC20(USDC).forceApprove(OLD_WA_GLOBAL_DOLLAR_USDC, 100e6); vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, 0)); - ITokenizationSpoke(OLD_WA_PAXOS_USDC).deposit(100e6, USER); + ITokenizationSpoke(OLD_WA_GLOBAL_DOLLAR_USDC).deposit(100e6, USER); - IERC20(USDT).forceApprove(OLD_WA_PAXOS_USDT, 100e6); + IERC20(USDT).forceApprove(OLD_WA_GLOBAL_DOLLAR_USDT, 100e6); vm.expectRevert(abi.encodeWithSelector(IHub.AddCapExceeded.selector, 0)); - ITokenizationSpoke(OLD_WA_PAXOS_USDT).deposit(100e6, USER); + ITokenizationSpoke(OLD_WA_GLOBAL_DOLLAR_USDT).deposit(100e6, USER); vm.stopPrank(); } function test_oldUsdcSpoke_withdrawalsOpen() public onlyDevnet { - ITokenizationSpoke oldSpoke = ITokenizationSpoke(OLD_WA_PAXOS_USDC); - uint256 shares = oldSpoke.balanceOf(OLD_WA_PAXOS_USDC_HOLDER); + ITokenizationSpoke oldSpoke = ITokenizationSpoke(OLD_WA_GLOBAL_DOLLAR_USDC); + uint256 shares = oldSpoke.balanceOf(OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER); assertGt(shares, 0); - uint256 balanceBefore = IERC20(USDC).balanceOf(OLD_WA_PAXOS_USDC_HOLDER); - vm.prank(OLD_WA_PAXOS_USDC_HOLDER); - uint256 assets = oldSpoke.redeem(shares, OLD_WA_PAXOS_USDC_HOLDER, OLD_WA_PAXOS_USDC_HOLDER); + uint256 balanceBefore = IERC20(USDC).balanceOf(OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER); + vm.prank(OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER); + uint256 assets = oldSpoke.redeem( + shares, + OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER, + OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER + ); assertGt(assets, 0); assertEq( - IERC20(USDC).balanceOf(OLD_WA_PAXOS_USDC_HOLDER), + IERC20(USDC).balanceOf(OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER), balanceBefore + assets, 'holder should be able to fully exit the frozen spoke' ); - assertEq(oldSpoke.balanceOf(OLD_WA_PAXOS_USDC_HOLDER), 0); + assertEq(oldSpoke.balanceOf(OLD_WA_GLOBAL_DOLLAR_USDC_HOLDER), 0); } function _depositAndRedeem(address spoke, address underlying, uint256 amount) internal { @@ -194,7 +205,7 @@ contract PaxosTokenizationSpokesActivationTest is Test { address underlying, uint40 expectedAddCap ) internal view { - IHub hub = IHub(PAXOS_HUB); + IHub hub = IHub(GLOBAL_DOLLAR_HUB); assertTrue(hub.isSpokeListed(assetId, spoke)); assertEq(ITokenizationSpoke(spoke).asset(), underlying); diff --git a/tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol b/tests/scripts/AaveV4DeployGlobalDollarTokenizationSpokes.t.sol similarity index 62% rename from tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol rename to tests/scripts/AaveV4DeployGlobalDollarTokenizationSpokes.t.sol index 8df4ff3cb..563b4923a 100644 --- a/tests/scripts/AaveV4DeployPaxosTokenizationSpokes.t.sol +++ b/tests/scripts/AaveV4DeployGlobalDollarTokenizationSpokes.t.sol @@ -9,13 +9,15 @@ import {ITokenizationSpoke} from 'src/spoke/interfaces/ITokenizationSpoke.sol'; import {BatchReports} from 'src/deployments/libraries/BatchReports.sol'; import {ProxyHelper} from 'tests/utils/ProxyHelper.sol'; -import {AaveV4DeployPaxosTokenizationSpokes} from 'scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol'; +import {AaveV4DeployGlobalDollarTokenizationSpokes} from 'scripts/deploy/AaveV4DeployTokenizationSpoke.s.sol'; -contract AaveV4DeployPaxosTokenizationSpokesTest is Test { +contract AaveV4DeployGlobalDollarTokenizationSpokesTest is Test { // deprecated instances whose ProxyAdmins are owned by the PayloadsController - address internal constant DEPRECATED_WA_PAXOS_USDC = 0x4131E0B2E7AFeCEAf3d3b4225aA61a3B2B7535b8; - address internal constant DEPRECATED_WA_PAXOS_USDT = 0x8Dabe53E8cB991c57f0307F6f419E6D469b0deAA; - address internal constant DEPRECATED_WA_PAXOS_PT_USDG = + address internal constant DEPRECATED_WA_GLOBAL_DOLLAR_USDC = + 0x4131E0B2E7AFeCEAf3d3b4225aA61a3B2B7535b8; + address internal constant DEPRECATED_WA_GLOBAL_DOLLAR_USDT = + 0x8Dabe53E8cB991c57f0307F6f419E6D469b0deAA; + address internal constant DEPRECATED_WA_GLOBAL_DOLLAR_PT_USDG = 0x27eF1140364948A0E30E248297FfDFE5a4091ec4; // GovernanceV3Ethereum.PAYLOADS_CONTROLLER address internal constant PAYLOADS_CONTROLLER = 0xdAbad81aF85554E9ae636395611C58F7eC1aAEc5; @@ -25,11 +27,11 @@ contract AaveV4DeployPaxosTokenizationSpokesTest is Test { address internal constant CORE_USDC_TOKENIZATION_SPOKE = 0x531E90a2376902DE8915789Fcc1075e3B0c153E7; - AaveV4DeployPaxosTokenizationSpokes internal _script; + AaveV4DeployGlobalDollarTokenizationSpokes internal _script; function setUp() public { vm.createSelectFork(vm.rpcUrl('mainnet'), 25544900); - _script = new AaveV4DeployPaxosTokenizationSpokes(); + _script = new AaveV4DeployGlobalDollarTokenizationSpokes(); } function test_run_deploysTokenizationSpokes() public { @@ -38,15 +40,19 @@ contract AaveV4DeployPaxosTokenizationSpokesTest is Test { address[3] memory underlyings = [_script.USDC(), _script.USDT(), _script.PT_USDG_24SEP2026()]; string[3] memory names = [ - 'Wrapped Aave Paxos USDC', - 'Wrapped Aave Paxos USDT', - 'Wrapped Aave Paxos PT_USDG_24SEP2026' + 'Wrapped Aave Global Dollar USDC', + 'Wrapped Aave Global Dollar USDT', + 'Wrapped Aave Global Dollar PT_USDG_24SEP2026' + ]; + string[3] memory symbols = [ + 'waGlobalDollarUSDC', + 'waGlobalDollarUSDT', + 'waGlobalDollarPT_USDG_24SEP2026' ]; - string[3] memory symbols = ['waPaxosUSDC', 'waPaxosUSDT', 'waPaxosPT_USDG_24SEP2026']; address[3] memory deprecated = [ - DEPRECATED_WA_PAXOS_USDC, - DEPRECATED_WA_PAXOS_USDT, - DEPRECATED_WA_PAXOS_PT_USDG + DEPRECATED_WA_GLOBAL_DOLLAR_USDC, + DEPRECATED_WA_GLOBAL_DOLLAR_USDT, + DEPRECATED_WA_GLOBAL_DOLLAR_PT_USDG ]; for (uint256 i; i < reports.length; ++i) { @@ -55,7 +61,7 @@ contract AaveV4DeployPaxosTokenizationSpokesTest is Test { assertGt(reports[i].tokenizationSpokeImplementation.code.length, 0); assertNotEq(proxy, deprecated[i]); - assertEq(ITokenizationSpoke(proxy).hub(), _script.PAXOS_HUB()); + assertEq(ITokenizationSpoke(proxy).hub(), _script.GLOBAL_DOLLAR_HUB()); assertEq(ITokenizationSpoke(proxy).asset(), underlyings[i]); assertEq(ITokenizationSpoke(proxy).name(), names[i]); assertEq(ITokenizationSpoke(proxy).symbol(), symbols[i]); @@ -88,7 +94,7 @@ contract AaveV4DeployPaxosTokenizationSpokesTest is Test { } function test_constantsMatchOnchainState() public view { - assertEq(_script.PAXOS_HUB(), 0x62d63197660c080236193CA60b70E49A08E90368); + assertEq(_script.GLOBAL_DOLLAR_HUB(), 0x62d63197660c080236193CA60b70E49A08E90368); // the intended owner is the owner of the healthy mainnet TokenizationSpoke ProxyAdmins assertEq( @@ -97,12 +103,24 @@ contract AaveV4DeployPaxosTokenizationSpokesTest is Test { ); // deploy inputs must match the deprecated instances they replace - assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDC).asset(), _script.USDC()); - assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDT).asset(), _script.USDT()); - assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_PT_USDG).asset(), _script.PT_USDG_24SEP2026()); - assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDC).hub(), _script.PAXOS_HUB()); - assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_USDT).hub(), _script.PAXOS_HUB()); - assertEq(ITokenizationSpoke(DEPRECATED_WA_PAXOS_PT_USDG).hub(), _script.PAXOS_HUB()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_GLOBAL_DOLLAR_USDC).asset(), _script.USDC()); + assertEq(ITokenizationSpoke(DEPRECATED_WA_GLOBAL_DOLLAR_USDT).asset(), _script.USDT()); + assertEq( + ITokenizationSpoke(DEPRECATED_WA_GLOBAL_DOLLAR_PT_USDG).asset(), + _script.PT_USDG_24SEP2026() + ); + assertEq( + ITokenizationSpoke(DEPRECATED_WA_GLOBAL_DOLLAR_USDC).hub(), + _script.GLOBAL_DOLLAR_HUB() + ); + assertEq( + ITokenizationSpoke(DEPRECATED_WA_GLOBAL_DOLLAR_USDT).hub(), + _script.GLOBAL_DOLLAR_HUB() + ); + assertEq( + ITokenizationSpoke(DEPRECATED_WA_GLOBAL_DOLLAR_PT_USDG).hub(), + _script.GLOBAL_DOLLAR_HUB() + ); } function test_tokenizationSpokeSaltMatchesOrchestrationFormula_fuzz( @@ -112,8 +130,10 @@ contract AaveV4DeployPaxosTokenizationSpokesTest is Test { bytes32 userSalt = keccak256(bytes('chain 1_version 1')); bytes32 expectedRoot = bytes32(bytes20(deployer)) | (keccak256(abi.encode(orchestrationSalt, userSalt)) >> 160); - bytes32 expected = keccak256(abi.encode(expectedRoot, 'tokenization-spoke', 'waPaxosUSDC')); + bytes32 expected = keccak256( + abi.encode(expectedRoot, 'tokenization-spoke', 'waGlobalDollarUSDC') + ); - assertEq(_script.tokenizationSpokeSalt(deployer, 'waPaxosUSDC'), expected); + assertEq(_script.tokenizationSpokeSalt(deployer, 'waGlobalDollarUSDC'), expected); } }