Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion scripts/utils/SpokeDeployUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ library SpokeDeployUtils {

/// @notice Deploys LiquidationLogic via CREATE2.
/// @dev The CREATE2 factory must already be deployed on the target chain.
/// @dev Idempotent: the salt is a fixed constant and the Safe Singleton Factory is
/// permissionless, so the deterministic address can already be occupied, either by a
/// previous run of this step or by a third party. Any contract at that address must have
/// been created from this exact creation code, so reusing it is safe and keeps this
/// mandatory first deployment step from being blockable or non-repeatable.
/// @param salt The CREATE2 salt for deterministic deployment.
/// @return The deployed library address.
function deployLiquidationLogic(bytes32 salt) internal returns (address) {
bytes memory bytecode = vm.getCode('src/spoke/libraries/LiquidationLogic.sol:LiquidationLogic');
return Create2Utils.create2Deploy(salt, bytecode);
return Create2Utils.create2DeployIdempotent(salt, bytecode);
}

/// @notice Returns the FOUNDRY_LIBRARIES-compatible string for library linking.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4AccessManagerEnumerableDeployProcedure is AaveV4DeployProcedureBa
function _deployAccessManagerEnumerable(address admin, bytes32 salt) internal returns (address) {
require(admin != address(0), 'invalid admin');
return
Create2Utils.create2Deploy(
Create2Utils.create2DeployIdempotent(
salt,
abi.encodePacked(type(AccessManagerEnumerable).creationCode, abi.encode(admin))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4HubConfiguratorDeployProcedure is AaveV4DeployProcedureBase {
function _deployHubConfigurator(address authority, bytes32 salt) internal returns (address) {
require(authority != address(0), 'invalid authority');
return
Create2Utils.create2Deploy(
Create2Utils.create2DeployIdempotent(
salt,
abi.encodePacked(type(HubConfigurator).creationCode, abi.encode(authority))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ contract AaveV4HubDeployProcedure is AaveV4DeployProcedureBase {
) internal returns (address hubProxy, address hubImplementation) {
require(proxyAdminOwner != address(0), 'invalid proxy admin owner');
require(authority != address(0), 'invalid authority');
hubImplementation = Create2Utils.create2Deploy({salt: salt, bytecode: hubBytecode});
hubImplementation = Create2Utils.create2DeployIdempotent({salt: salt, bytecode: hubBytecode});
hubProxy = Create2Utils.proxify({
salt: salt,
logic: hubImplementation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4InterestRateStrategyDeployProcedure is AaveV4DeployProcedureBase
function _deployInterestRateStrategy(address hub, bytes32 salt) internal returns (address) {
require(hub != address(0), 'invalid hub');
return
Create2Utils.create2Deploy(
Create2Utils.create2DeployIdempotent(
salt,
abi.encodePacked(type(AssetInterestRateStrategy).creationCode, abi.encode(hub))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4ConfigPositionManagerDeployProcedure is AaveV4DeployProcedureBase
function _deployConfigPositionManager(address owner, bytes32 salt) internal returns (address) {
require(owner != address(0), 'invalid owner');
return
Create2Utils.create2Deploy({
Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: abi.encodePacked(type(ConfigPositionManager).creationCode, abi.encode(owner))
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4GiverPositionManagerDeployProcedure is AaveV4DeployProcedureBase
function _deployGiverPositionManager(address owner, bytes32 salt) internal returns (address) {
require(owner != address(0), 'invalid owner');
return
Create2Utils.create2Deploy({
Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: abi.encodePacked(type(GiverPositionManager).creationCode, abi.encode(owner))
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract AaveV4NativeTokenGatewayDeployProcedure is AaveV4DeployProcedureBase {
require(nativeWrapper != address(0), 'invalid native wrapper');
require(owner != address(0), 'invalid owner');
return
Create2Utils.create2Deploy({
Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: abi.encodePacked(
type(NativeTokenGateway).creationCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4SignatureGatewayDeployProcedure is AaveV4DeployProcedureBase {
function _deploySignatureGateway(address owner, bytes32 salt) internal returns (address) {
require(owner != address(0), 'invalid owner');
return
Create2Utils.create2Deploy({
Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: abi.encodePacked(type(SignatureGateway).creationCode, abi.encode(owner))
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract AaveV4TakerPositionManagerDeployProcedure is AaveV4DeployProcedureBase
function _deployTakerPositionManager(address owner, bytes32 salt) internal returns (address) {
require(owner != address(0), 'invalid owner');
return
Create2Utils.create2Deploy({
Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: abi.encodePacked(type(TakerPositionManager).creationCode, abi.encode(owner))
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ contract AaveV4SpokeConfiguratorDeployProcedure is AaveV4DeployProcedureBase {
function _deploySpokeConfigurator(address authority, bytes32 salt) internal returns (address) {
require(authority != address(0), 'invalid authority');
return
Create2Utils.create2Deploy(
Create2Utils.create2DeployIdempotent(
salt,
abi.encodePacked(type(SpokeConfigurator).creationCode, abi.encode(authority))
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ contract AaveV4SpokeDeployProcedure is AaveV4DeployProcedureBase {
require(authority != address(0), 'invalid authority');
require(oracle != address(0), 'invalid oracle');
require(maxUserReservesLimit > 0, 'invalid max user reserves limit');
spokeImplementation = Create2Utils.create2Deploy({
spokeImplementation = Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: _getSpokeInstanceInitCode(spokeBytecode, oracle, maxUserReservesLimit)
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ contract AaveV4TokenizationSpokeDeployProcedure is AaveV4DeployProcedureBase {
require(bytes(shareName).length > 0, 'invalid share name');
require(bytes(shareSymbol).length > 0, 'invalid share symbol');

tokenizationSpokeImplementation = Create2Utils.create2Deploy({
tokenizationSpokeImplementation = Create2Utils.create2DeployIdempotent({
salt: salt,
bytecode: _getTokenizationSpokeInstanceInitCode(hub, underlying)
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ contract AaveV4TreasurySpokeDeployProcedure is AaveV4DeployProcedureBase {
/// @return The address of the deployed transparent proxy contract.
function _deployTreasurySpoke(address owner, bytes32 salt) internal returns (address) {
require(owner != address(0), 'invalid owner');
address implementation = Create2Utils.create2Deploy(
address implementation = Create2Utils.create2DeployIdempotent(
salt,
type(TreasurySpokeInstance).creationCode
);
Expand Down
35 changes: 34 additions & 1 deletion src/deployments/utils/libraries/Create2Utils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ library Create2Utils {
// https://github.com/safe-global/safe-singleton-factory
address public constant CREATE2_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7;

/// @notice Emitted when a contract already present at the computed CREATE2 address is adopted
/// instead of being deployed again.
/// @dev Absence of this event on a deployment step means the contract was freshly deployed.
/// Its presence means the step was a no-op adoption, which is the signal an operator needs
/// to notice an accidental re-run or a third party having occupied the address first.
/// @param deployed The address that was adopted.
/// @param salt The CREATE2 salt the address was derived from.
event Create2DeploymentAdopted(address indexed deployed, bytes32 salt);

error MissingCreate2Factory();
error Create2AddressDerivationFailure();
error FailedCreate2FactoryCall();
Expand All @@ -32,7 +41,31 @@ library Create2Utils {
return deployedAt;
}

/// @notice Deploys a contract via CREATE2, returning the existing address if it is already
/// deployed instead of reverting.
/// @dev Safe because a CREATE2 address commits to `keccak256(bytecode)`: any contract living at
/// the computed address must have been created by this factory running this exact creation
/// code, so its runtime code is necessarily identical to what this call would produce.
/// Use this for steps that must be idempotent and must not be blockable by a third party
/// occupying the deterministic address first.
/// @param salt The CREATE2 salt.
/// @param bytecode The contract creation bytecode.
/// @return The deployed contract address.
function create2DeployIdempotent(bytes32 salt, bytes memory bytecode) internal returns (address) {
address computed = computeCreate2Address({salt: salt, bytecode: bytecode});
if (isContractDeployed(computed)) {
emit Create2DeploymentAdopted(computed, salt);
return computed;
}
return create2Deploy({salt: salt, bytecode: bytecode});
}

/// @notice Deploys a TransparentUpgradeableProxy via CREATE2.
/// @dev Idempotent, matching the deployment procedures. The proxy admin owner and the
/// initializer calldata are both constructor arguments, so they are covered by the init
/// code hash the CREATE2 address commits to. No protocol initializer reads `msg.sender`,
/// block data or any other environment value, so a proxy already present at the computed
/// address is state-equivalent to the one this call would create.
/// @param salt The CREATE2 salt.
/// @param logic The implementation contract address.
/// @param initialOwner The initial proxy admin owner.
Expand All @@ -45,7 +78,7 @@ library Create2Utils {
bytes memory data
) internal returns (address) {
return
create2Deploy(
create2DeployIdempotent(
salt,
abi.encodePacked(
type(TransparentUpgradeableProxy).creationCode,
Expand Down
141 changes: 141 additions & 0 deletions tests/deployments/AaveV4DeployOrchestration.Idempotency.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import 'tests/utils/BatchTestProcedures.sol';

import {AccessManagerEnumerable} from 'src/access/AccessManagerEnumerable.sol';

/// @dev Deterministic CREATE2 deployment must be idempotent.
///
/// The orchestration derives its root salt as
/// `bytes32(bytes20(deployer)) | (keccak256(abi.encode(SALT, deployInputs.salt)) >> 160)`
/// which looks deployer-namespaced. But the Safe Singleton Factory does not enforce the salt
/// prefix, so namespacing only helps if the salt is UNPREDICTABLE. When `deployInputs.salt` is
/// left at `bytes32(0)` — which the deploy script permits with a warning only, and which the
/// repo's own deployment tests use — every input is public and the whole root salt is computable
/// in advance from the (public) deployer address alone.
contract AaveV4DeployOrchestrationIdempotencyTest is BatchTestProcedures {
address internal ATTACKER = makeAddr('ATTACKER');

function setUp() public override {
super.setUp();

_inputs = InputUtils.FullDeployInputs({
accessManagerAdmin: makeAddr('accessManagerAdmin'),
proxyAdminOwner: makeAddr('proxyAdminOwner'),
hubAdmin: makeAddr('hubAdmin'),
hubConfiguratorAdmin: makeAddr('hubConfiguratorAdmin'),
treasurySpokeOwner: makeAddr('treasurySpokeOwner'),
spokeAdmin: makeAddr('spokeAdmin'),
spokeConfiguratorAdmin: makeAddr('spokeConfiguratorAdmin'),
gatewayOwner: makeAddr('gatewayOwner'),
positionManagerOwner: makeAddr('positionManagerOwner'),
nativeWrapper: _weth9,
deployNativeTokenGateway: false,
deploySignatureGateway: false,
deployPositionManagers: false,
grantRoles: true,
hubLabels: _hubLabels,
spokeLabels: _spokeLabels,
spokeMaxReservesLimits: _defaultSpokeMaxReservesLimits(_spokeLabels.length),
salt: bytes32(0)
});
}

/// @dev Reproduces AaveV4DeployOrchestration._deriveSalt using only public information.
function _rootSalt(address deployer, bytes32 userSalt) internal pure returns (bytes32) {
return
bytes32(bytes20(deployer)) |
(keccak256(abi.encode(keccak256('AAVE_V4'), userSalt)) >> 160);
}

/// @dev External so `vm.expectRevert` binds to the deployment frame; `deployAaveV4` is an
/// internal library and would otherwise be inlined at test depth.
function runDeploymentExternal() external {
vm.startPrank(_deployer);
AaveV4DeployOrchestration.deployAaveV4(
_logger,
_deployer,
_inputs,
BytecodeHelper.getHubBytecode(),
BytecodeHelper.getSpokeBytecode()
);
vm.stopPrank();
}

function _runDeployment() internal {
this.runDeploymentExternal();
}

/// @dev Control: the deployment works when nobody interferes.
function test_baseline_deploymentSucceeds() public {
_runDeployment();
}

/// @dev PROOF: with the zero user salt, the AccessManagerEnumerable address — the very first
/// contract of the deployment — is computable by anyone, and its init code has no precondition
/// (constructor only stores an admin), so a third party can occupy it and brick the whole run.
function test_attackerOccupiesAccessManager_deploymentStillCompletes() public {
bytes32 salt = _rootSalt(_deployer, bytes32(0));

// Root admin is the deployer itself (AaveV4DeployOrchestration sets initialAdmin = deployer).
bytes memory initCode = abi.encodePacked(
type(AccessManagerEnumerable).creationCode,
abi.encode(_deployer)
);
address predicted = Create2Utils.computeCreate2Address(salt, initCode);

assertEq(predicted.code.length, 0, 'address free before the attack');

vm.prank(ATTACKER);
(bool ok, ) = Create2Utils.CREATE2_FACTORY.call(abi.encodePacked(salt, initCode));
assertTrue(ok, 'attacker can deploy it: no unmet constructor precondition');
assertGt(predicted.code.length, 0, 'attacker occupies the AccessManager address');

// BEFORE THE FIX: reverted with Create2Utils.ContractAlreadyDeployed, bricking the run.
// AFTER THE FIX: the deployment reuses the identical contract and completes.
this.runDeploymentExternal();
assertGt(predicted.code.length, 0, 'deployment completed reusing the occupied address');
}

/// @dev The Hub implementation is the softest target of all: `create2Deploy` is called with the
/// raw compiled bytecode and NO constructor arguments, so its init code is a public constant.
/// Only the child salt is needed, and that is derived from the root salt plus the hub label.
function test_attackerOccupiesHubImplementation_deploymentStillCompletes() public {
bytes32 salt = _rootSalt(_deployer, bytes32(0));
bytes32 childSalt = keccak256(abi.encode(salt, 'hub', _hubLabels[0]));

bytes memory hubBytecode = BytecodeHelper.getHubBytecode();
address predicted = Create2Utils.computeCreate2Address(childSalt, hubBytecode);

vm.prank(ATTACKER);
(bool ok, ) = Create2Utils.CREATE2_FACTORY.call(abi.encodePacked(childSalt, hubBytecode));
assertTrue(ok, 'HubInstance has no constructor args and no preconditions');
assertGt(predicted.code.length, 0, 'attacker occupies the Hub implementation address');

// BEFORE THE FIX: reverted with Create2Utils.ContractAlreadyDeployed.
this.runDeploymentExternal();
assertGt(predicted.code.length, 0, 'deployment completed reusing the occupied address');
}

/// @dev A non-zero, unguessable user salt removes the pre-positioning capability: the attacker
/// cannot compute the address ahead of the deployment transaction.
function test_nonZeroSecretSalt_addressIsNotPrecomputable() public {
bytes32 secret = keccak256('operator chosen secret salt');
bytes32 guessed = _rootSalt(_deployer, bytes32(0));
bytes32 actual = _rootSalt(_deployer, secret);

assertTrue(guessed != actual, 'a secret user salt changes the derived root salt');

_inputs.salt = secret;
// Occupying the address the attacker CAN compute (zero-salt derivation) is now harmless.
bytes memory initCode = abi.encodePacked(
type(AccessManagerEnumerable).creationCode,
abi.encode(_deployer)
);
vm.prank(ATTACKER);
Create2Utils.CREATE2_FACTORY.call(abi.encodePacked(guessed, initCode));

_runDeployment();
}
}
Loading