Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
20 changes: 17 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@
update:; forge update

# Build & test
build :; forge build --sizes
test :; forge test -vvv
build-simple :; forge build
build-simple-sizes :; forge build --sizes
test-simple :; forge test -vvv
generate :; forge script scripts/utils/GenerateBytecodeScript.s.sol
build :
make build-simple
make generate
build-sizes :
make build-simple-sizes
make generate
test :
make build
make test-simple

# Utilities
download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address}
Expand All @@ -16,7 +27,10 @@ git-diff :
@npx prettier ${before} ${after} --write
@printf '%s\n%s\n%s\n' "\`\`\`diff" "$$(git diff --no-index --diff-algorithm=patience --ignore-space-at-eol ${before} ${after})" "\`\`\`" > diffs/${out}.md

gas-report :; forge test --mp 'tests/gas/**'
gas-report :
make build
make test-gas-report
test-gas-report :; forge test --mp 'tests/gas/**'

# Coverage
coverage-base :; FOUNDRY_PROFILE=coverage forge coverage --report lcov --no-match-coverage "(scripts|tests|deployments|mocks)"
Expand Down
6 changes: 5 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ src = 'src'
test = 'tests'
out = 'out'
libs = ['lib']
fs_permissions = [{ access = "read", path = "tests/mocks/JsonBindings.sol" }]
fs_permissions = [
{ access = "read", path = "tests/mocks/JsonBindings.sol" },
{ access = "read-write", path = "./tests/bin" },
{ access = "read", path = "out/" }
]
solc_version = "0.8.28"
evm_version = "cancun"
optimizer = true
Expand Down
19 changes: 19 additions & 0 deletions scripts/utils/GenerateBytecodeScript.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {Script} from 'forge-std/Script.sol';

contract GenerateHubBytecodeScript is Script {
Comment thread
Kogaroshi marked this conversation as resolved.
string private constant HUB_BYTECODE_PATH = 'tests/bin/hub.bytecode';
string private constant SPOKE_INSTANCE_BYTECODE_PATH = 'tests/bin/spokeInstance.bytecode';

function run() external {
bytes memory hubBytecode = vm.getCode('src/hub/Hub.sol:Hub');
vm.writeFileBinary(HUB_BYTECODE_PATH, hubBytecode);

string memory artifact = vm.readFile('out/SpokeInstance.sol/SpokeInstance.json');
string memory spokeHex = vm.parseJsonString(artifact, '.bytecode.object');
Comment thread
Kogaroshi marked this conversation as resolved.
vm.writeFile(SPOKE_INSTANCE_BYTECODE_PATH, spokeHex);
Comment thread
Kogaroshi marked this conversation as resolved.
}
Comment thread
Kogaroshi marked this conversation as resolved.
}
1 change: 1 addition & 0 deletions tests/DeployUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pragma solidity ^0.8.0;

import {Vm} from 'forge-std/Vm.sol';

import {TransparentUpgradeableProxy} from 'src/dependencies/openzeppelin/TransparentUpgradeableProxy.sol';
import {IHub} from 'src/hub/interfaces/IHub.sol';
import {ISpoke} from 'src/spoke/interfaces/ISpoke.sol';
Expand Down
Binary file added tests/bin/hub.bytecode
Binary file not shown.
1 change: 1 addition & 0 deletions tests/bin/spokeInstance.bytecode

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions tests/unit/scripts/GenerateBytecode.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {Test} from 'forge-std/Test.sol';

import {BytecodeLoader} from 'tests/utils/BytecodeLoader.sol';

contract GenerateBytecodeScript is Test {
Comment thread
Kogaroshi marked this conversation as resolved.
string private constant HUB_BYTECODE_PATH = 'tests/bin/hub.bytecode';
string private constant SPOKE_INSTANCE_BYTECODE_PATH = 'tests/bin/spokeInstance.bytecode';
Comment thread
Kogaroshi marked this conversation as resolved.

function test_generated_HubBytecode() public view {
bytes memory hubBytecode = vm.getCode('src/hub/Hub.sol:Hub');

assertEq(
hubBytecode,
BytecodeLoader.loadHubBytecode(),
'Loaded Hub bytecode does not match generated bytecode'
);
}

function test_generated_SpokeInstanceBytecode() public view {
bytes memory spokeInstanceBytecode = vm.getCode(
'src/spoke/instances/SpokeInstance.sol:SpokeInstance'
);

assertEq(
spokeInstanceBytecode,
BytecodeLoader.loadSpokeInstanceBytecode(),
'Loaded SpokeInstance bytecode does not match generated bytecode'
);
}
}
32 changes: 32 additions & 0 deletions tests/utils/BytecodeLoader.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {Vm} from 'forge-std/Vm.sol';

import {LiquidationLogic} from 'src/spoke/libraries/LiquidationLogic.sol';

library BytecodeLoader {
Vm private constant vm = Vm(address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))));

string private constant HUB_BYTECODE_PATH = 'tests/bin/hub.bytecode';
string private constant SPOKE_INSTANCE_BYTECODE_PATH = 'tests/bin/spokeInstance.bytecode';

Comment thread
Kogaroshi marked this conversation as resolved.
string private constant LIQUIDATION_LOGIC_PLACEHOLDER =
'__$a48140799943db40fec4e369e92a011fa5$__';
Comment thread
Kogaroshi marked this conversation as resolved.

function loadHubBytecode() public view returns (bytes memory) {
return vm.readFileBinary(HUB_BYTECODE_PATH);
}

function loadSpokeInstanceBytecode() public view returns (bytes memory) {
string memory hexBytecode = vm.readFile(SPOKE_INSTANCE_BYTECODE_PATH);
string memory addrHex = vm.replace(
vm.toString(abi.encodePacked(address(LiquidationLogic))),
'0x',
''
);
string memory linked = vm.replace(hexBytecode, LIQUIDATION_LOGIC_PLACEHOLDER, addrHex);
return vm.parseBytes(linked);
}
Comment thread
Kogaroshi marked this conversation as resolved.
}
22 changes: 22 additions & 0 deletions tests/utils/DeployHub.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {BytecodeLoader} from 'tests/utils/BytecodeLoader.sol';

import {Create2Utils} from 'tests/Create2Utils.sol';

/// @notice Helper to deploy Hub from custom profile precompiled bytecode
contract DeployHub {
/// @notice Deploys a Hub contract using CREATE2 and the stored bytecode with the provided deployment arguments.
/// @param deployArgs The constructor arguments for the Hub contract, encoded as bytes.
/// @param salt The salt to use for the CREATE2 deployment, allowing for deterministic address generation.
/// @return The address of the deployed Hub contract.
function deployHub(bytes memory deployArgs, bytes32 salt) public returns (address) {
Create2Utils.loadCreate2Factory();

bytes memory bytecode = BytecodeLoader.loadHubBytecode();

return Create2Utils.create2Deploy(salt, abi.encodePacked(bytecode, deployArgs));
}
}
32 changes: 32 additions & 0 deletions tests/utils/DeploySpoke.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (c) 2025 Aave Labs
pragma solidity ^0.8.0;

import {BytecodeLoader} from 'tests/utils/BytecodeLoader.sol';

import {Create2Utils} from 'tests/Create2Utils.sol';
import {DeployUtils} from 'tests/DeployUtils.sol';

/// @notice Helper to deploy Spoke from custom profile precompiled bytecode
contract DeploySpoke {
/// @notice Deploys a proxified SpokeInstance contract using CREATE2 and the stored bytecode with the provided deployment arguments, and proxified using initialize arguments.
/// @param deployArgs The constructor arguments for the SpokeInstance implementation contract, encoded as bytes.
/// @param initArgs The initialization arguments for the TransparentUpgradeableProxy, encoded as bytes.
/// @param salt The salt to use for the CREATE2 deployment, allowing for deterministic address generation.
/// @return The address of the deployed SpokeInstance implementation
/// @return The address of the deployed SpokeInstance proxy
function deploySpoke(
bytes memory deployArgs,
bytes memory initArgs,
bytes32 salt
) public returns (address, address) {
Create2Utils.loadCreate2Factory();

bytes memory bytecode = BytecodeLoader.loadSpokeInstanceBytecode();

address impl = Create2Utils.create2Deploy(salt, abi.encodePacked(bytecode, deployArgs));
address proxy = DeployUtils.proxify(impl, msg.sender, initArgs);

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deploySpoke function automatically sets msg.sender as the proxy admin owner by calling DeployUtils.proxify(impl, msg.sender, initArgs). This might not be the desired behavior for all use cases, as external consumers may want to specify a different proxy admin owner. Consider either: 1) Adding an additional parameter for the proxy admin owner, or 2) Adding a comment that clearly documents that msg.sender will become the proxy admin owner. The existing DeployUtils.deploySpoke function accepts an explicit proxyAdminOwner parameter, so this inconsistency could cause confusion.

Copilot uses AI. Check for mistakes.

return (impl, proxy);
}
}
Loading