diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e50369..4481ec6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,11 @@ jobs: run: | forge --version + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + - name: Run Forge build run: | forge build --sizes diff --git a/.gitignore b/.gitignore index 85198aa..7580575 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ docs/ # Dotenv file .env + +# OS files +.DS_Store \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 44b45b7..2e78fca 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ +.github/ foundry.toml out/ lib/ -cache/ +cache/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index c59b1cd..68c2c17 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,19 +1,22 @@ { "printWidth": 120, "tabWidth": 4, - "useTabs": true, "singleQuote": false, "trailingComma": "all", "overrides": [ { "files": "*.sol", "options": { + "useTabs": false, "bracketSpacing": false } }, { - "files": ["*.json", "*.js", "*.ts"], - "options": {} + "files": "*.json", + "options": { + "useTabs": true, + "bracketSpacing": true + } } ] } diff --git a/foundry.toml b/foundry.toml index 9079a84..c5f4e2f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -29,6 +29,14 @@ remappings = [ "createx/=lib/createx/src" ] +[fmt] +line_length = 120 +tab_width = 4 +quote_style = "double" +func_attrs_with_params_multiline = true +inline_attribute_style = "compact" +return_statement = "inline" + [fuzz] runs = 5000 max_test_rejects = 1000000 diff --git a/lib/createx b/lib/createx index 5da6242..ed873f9 160000 --- a/lib/createx +++ b/lib/createx @@ -1 +1 @@ -Subproject commit 5da62420fb21f90b6a24deb370caee8eb8328a81 +Subproject commit ed873f9523f7951336984efb64013349a996dd32 diff --git a/script/BaseScript.sol b/script/BaseScript.sol index d3c247c..6fc8173 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -2,104 +2,103 @@ pragma solidity ^0.8.30; import {Script, stdJson} from "forge-std/Script.sol"; -import {ProxyForge} from "src/ProxyForge.sol"; abstract contract BaseScript is Script { - using stdJson for string; - - string private constant DEFAULT_MNEMONIC = "test test test test test test test test test test test junk"; - - address internal broadcaster; - - modifier broadcast() { - vm.startBroadcast(broadcaster); - _; - vm.stopBroadcast(); - } - - modifier fork(string memory chainAlias) { - vm.createSelectFork(chainAlias); - _; - } - - function setUp() public virtual { - broadcaster = vm.rememberKey(configurePrivateKey()); - } - - function configurePrivateKey() internal view virtual returns (uint256 privateKey) { - privateKey = vm.envOr({ - name: "PRIVATE_KEY", - defaultValue: vm.deriveKey({ - mnemonic: vm.envOr({name: "MNEMONIC", defaultValue: DEFAULT_MNEMONIC}), - index: uint8(vm.envOr({name: "EOA_INDEX", defaultValue: uint256(0)})) - }) - }); - } - - function generateJson(string memory path, string memory name, address instance, bytes32 salt) internal { - string memory json = "json"; - json.serialize("address", instance); - json.serialize("blockNumber", vm.getBlockNumber()); - json.serialize("name", name); - json.serialize("salt", salt); - json = json.serialize("timestamp", vm.getBlockTimestamp()); - json.write(path); - } - - function prompt(string memory promptText) internal returns (string memory input) { - return prompt(promptText, new string(0)); - } - - function prompt(string memory promptText, string memory defaultValue) internal returns (string memory input) { - input = vm.prompt(string.concat(promptText, " (default: `", defaultValue, "`)")); - if (bytes(input).length == 0) input = defaultValue; - } - - function promptAddress(string memory promptText, address defaultValue) internal returns (address) { - return vm.parseAddress(prompt(promptText, vm.toString(defaultValue))); - } - - function promptAddress(string memory promptText) internal returns (address) { - return promptAddress(promptText, address(0)); - } - - function promptBool(string memory promptText, bool defaultValue) internal returns (bool) { - return vm.parseBool(prompt(promptText, vm.toString(defaultValue))); - } - - function promptBool(string memory promptText) internal returns (bool) { - return promptBool(promptText, false); - } - - function promptUint256(string memory promptText, uint256 defaultValue) internal returns (uint256) { - return vm.parseUint(prompt(promptText, vm.toString(defaultValue))); - } - - function promptUint256(string memory promptText) internal returns (uint256) { - return promptUint256(promptText, uint256(0)); - } - - function promptInt256(string memory promptText, int256 defaultValue) internal returns (int256) { - return vm.parseInt(prompt(promptText, vm.toString(defaultValue))); - } - - function promptInt256(string memory promptText) internal returns (int256) { - return promptInt256(promptText, int256(0)); - } - - function promptBytes32(string memory promptText, bytes32 defaultValue) internal returns (bytes32) { - return vm.parseBytes32(prompt(promptText, vm.toString(defaultValue))); - } - - function promptBytes32(string memory promptText) internal returns (bytes32) { - return promptBytes32(promptText, bytes32(0)); - } - - function promptBytes(string memory promptText, bytes memory defaultValue) internal returns (bytes memory) { - return vm.parseBytes(prompt(promptText, vm.toString(defaultValue))); - } - - function promptBytes(string memory promptText) internal returns (bytes memory) { - return promptBytes(promptText, new bytes(0)); - } + using stdJson for string; + + string private constant DEFAULT_MNEMONIC = "test test test test test test test test test test test junk"; + + address internal broadcaster; + + modifier broadcast() { + vm.startBroadcast(broadcaster); + _; + vm.stopBroadcast(); + } + + modifier fork(string memory chainAlias) { + vm.createSelectFork(chainAlias); + _; + } + + function setUp() public virtual { + broadcaster = vm.rememberKey(configurePrivateKey()); + } + + function configurePrivateKey() internal view virtual returns (uint256 privateKey) { + privateKey = vm.envOr({ + name: "PRIVATE_KEY", + defaultValue: vm.deriveKey({ + mnemonic: vm.envOr({name: "MNEMONIC", defaultValue: DEFAULT_MNEMONIC}), + index: uint8(vm.envOr({name: "EOA_INDEX", defaultValue: uint256(0)})) + }) + }); + } + + function generateJson(string memory path, string memory name, address instance, bytes32 salt) internal { + string memory json = "json"; + json.serialize("address", instance); + json.serialize("blockNumber", vm.getBlockNumber()); + json.serialize("name", name); + json.serialize("salt", salt); + json = json.serialize("timestamp", vm.getBlockTimestamp()); + json.write(path); + } + + function prompt(string memory promptText) internal returns (string memory input) { + return prompt(promptText, new string(0)); + } + + function prompt(string memory promptText, string memory defaultValue) internal returns (string memory input) { + input = vm.prompt(string.concat(promptText, " (default: `", defaultValue, "`)")); + if (bytes(input).length == 0) input = defaultValue; + } + + function promptAddress(string memory promptText, address defaultValue) internal returns (address) { + return vm.parseAddress(prompt(promptText, vm.toString(defaultValue))); + } + + function promptAddress(string memory promptText) internal returns (address) { + return promptAddress(promptText, address(0)); + } + + function promptBool(string memory promptText, bool defaultValue) internal returns (bool) { + return vm.parseBool(prompt(promptText, vm.toString(defaultValue))); + } + + function promptBool(string memory promptText) internal returns (bool) { + return promptBool(promptText, false); + } + + function promptUint256(string memory promptText, uint256 defaultValue) internal returns (uint256) { + return vm.parseUint(prompt(promptText, vm.toString(defaultValue))); + } + + function promptUint256(string memory promptText) internal returns (uint256) { + return promptUint256(promptText, uint256(0)); + } + + function promptInt256(string memory promptText, int256 defaultValue) internal returns (int256) { + return vm.parseInt(prompt(promptText, vm.toString(defaultValue))); + } + + function promptInt256(string memory promptText) internal returns (int256) { + return promptInt256(promptText, int256(0)); + } + + function promptBytes32(string memory promptText, bytes32 defaultValue) internal returns (bytes32) { + return vm.parseBytes32(prompt(promptText, vm.toString(defaultValue))); + } + + function promptBytes32(string memory promptText) internal returns (bytes32) { + return promptBytes32(promptText, bytes32(0)); + } + + function promptBytes(string memory promptText, bytes memory defaultValue) internal returns (bytes memory) { + return vm.parseBytes(prompt(promptText, vm.toString(defaultValue))); + } + + function promptBytes(string memory promptText) internal returns (bytes memory) { + return promptBytes(promptText, new bytes(0)); + } } diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 816ee79..77ce10f 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -5,25 +5,27 @@ import {ProxyForge} from "src/ProxyForge.sol"; import {BaseScript} from "./BaseScript.sol"; contract Deploy is BaseScript { - string internal constant DEFAULT_CHAINS = "ethereum, optimism, polygon, base, arbitrum"; + string internal constant DEFAULT_CHAINS = "ethereum, optimism, polygon, base, arbitrum"; - bytes32 internal constant DEFAULT_SALT = 0x0000000000000000000000000000000000000000000050726f7879466f726765; + bytes32 internal constant DEFAULT_SALT = 0x0000000000000000000000000000000000000000000050726f7879466f726765; - bytes32 internal salt; + bytes32 internal salt; - function setUp() public virtual override { - super.setUp(); - salt = vm.envOr({name: "SALT", defaultValue: DEFAULT_SALT}); - } + function setUp() public virtual override { + super.setUp(); + salt = vm.envOr({name: "SALT", defaultValue: DEFAULT_SALT}); + } - function run() external { - string memory input = prompt("Chains separated by ','", DEFAULT_CHAINS); - string[] memory chains = vm.split(vm.replace(input, " ", ""), ","); - for (uint256 i; i < chains.length; ++i) deployOnChain(chains[i]); - } + function run() external { + string memory input = prompt("Chains separated by ','", DEFAULT_CHAINS); + string[] memory chains = vm.split(vm.replace(input, " ", ""), ","); + for (uint256 i; i < chains.length; ++i) { + deployOnChain(chains[i]); + } + } - function deployOnChain(string memory chainAlias) internal fork(chainAlias) broadcast { - string memory path = string.concat("./deployments/", vm.toString(block.chainid), ".json"); - generateJson(path, "ProxyForge", address(new ProxyForge{salt: salt}()), salt); - } + function deployOnChain(string memory chainAlias) internal fork(chainAlias) broadcast { + string memory path = string.concat("./deployments/", vm.toString(block.chainid), ".json"); + generateJson(path, "ProxyForge", address(new ProxyForge{salt: salt}()), salt); + } } diff --git a/script/DeployProxy.s.sol b/script/DeployProxy.s.sol index 1f88ff2..4ca1ade 100644 --- a/script/DeployProxy.s.sol +++ b/script/DeployProxy.s.sol @@ -5,24 +5,24 @@ import {IProxyForge} from "src/interfaces/IProxyForge.sol"; import {BaseScript} from "./BaseScript.sol"; contract DeployProxy is BaseScript { - IProxyForge internal constant FORGE = IProxyForge(0x58b819827cB18Ba425906C69E1Bfb22F27Cb1bCe); + IProxyForge internal constant FORGE = IProxyForge(0x58b819827cB18Ba425906C69E1Bfb22F27Cb1bCe); - function run() external broadcast returns (address proxy) { - require(address(FORGE).code.length != 0, "ProxyForge not exists"); + function run() external broadcast returns (address proxy) { + require(address(FORGE).code.length != 0, "ProxyForge not exists"); - address implementation = vm.promptAddress("Implementation"); - address owner = promptAddress("Owner", broadcaster); + address implementation = vm.promptAddress("Implementation"); + address owner = promptAddress("Owner", broadcaster); - bool isDeterministic = promptBool("Is Deterministic"); - bytes32 salt; - if (isDeterministic) salt = promptBytes32("Salt"); + bool isDeterministic = promptBool("Is Deterministic"); + bytes32 salt; + if (isDeterministic) salt = promptBytes32("Salt"); - bytes memory data = promptBytes("Data"); - uint256 value; - if (data.length != 0) value = promptUint256("msg.value"); + bytes memory data = promptBytes("Data"); + uint256 value; + if (data.length != 0) value = promptUint256("msg.value"); - proxy = isDeterministic - ? FORGE.deployDeterministicAndCall{value: value}(implementation, owner, salt, data) - : FORGE.deployAndCall{value: value}(implementation, owner, data); - } + proxy = isDeterministic + ? FORGE.deployDeterministicAndCall{value: value}(implementation, owner, salt, data) + : FORGE.deployAndCall{value: value}(implementation, owner, data); + } } diff --git a/script/UpgradeProxy.s.sol b/script/UpgradeProxy.s.sol index 2384f59..d2fba44 100644 --- a/script/UpgradeProxy.s.sol +++ b/script/UpgradeProxy.s.sol @@ -5,18 +5,18 @@ import {IProxyForge} from "src/interfaces/IProxyForge.sol"; import {BaseScript} from "./BaseScript.sol"; contract UpgradeProxy is BaseScript { - IProxyForge internal constant FORGE = IProxyForge(0x58b819827cB18Ba425906C69E1Bfb22F27Cb1bCe); + IProxyForge internal constant FORGE = IProxyForge(0x58b819827cB18Ba425906C69E1Bfb22F27Cb1bCe); - function run() external broadcast { - require(address(FORGE).code.length != 0, "ProxyForge not exists"); + function run() external broadcast { + require(address(FORGE).code.length != 0, "ProxyForge not exists"); - address proxy = vm.promptAddress("Proxy"); - address implementation = vm.promptAddress("Implementation"); + address proxy = vm.promptAddress("Proxy"); + address implementation = vm.promptAddress("Implementation"); - bytes memory data = promptBytes("Data"); - uint256 value; - if (data.length != 0) value = promptUint256("msg.value"); + bytes memory data = promptBytes("Data"); + uint256 value; + if (data.length != 0) value = promptUint256("msg.value"); - FORGE.upgradeAndCall{value: value}(proxy, implementation, data); - } + FORGE.upgradeAndCall{value: value}(proxy, implementation, data); + } } diff --git a/src/ProxyForge.sol b/src/ProxyForge.sol index 08bb2cc..29a76c2 100644 --- a/src/ProxyForge.sol +++ b/src/ProxyForge.sol @@ -8,305 +8,301 @@ import {ForgeProxy} from "src/proxy/ForgeProxy.sol"; /// @title ProxyForge /// @notice Factory contract for deploying, upgrading, and managing {ForgeProxy} instances contract ProxyForge is IProxyForge { - /// @notice Precomputed {ProxyDeployed} event signature - /// @dev keccak256(bytes("ProxyDeployed(address,address,bytes32)")) - uint256 private constant PROXY_DEPLOYED_EVENT_SIGNATURE = - 0xd283ed05905c0eb69fe3ef042c6ad706d8d9c75b138624098de540fa2c011a05; - - /// @notice Precomputed {ProxyUpgraded} event signature - /// @dev keccak256(bytes("ProxyUpgraded(address,address)")) - uint256 private constant PROXY_UPGRADED_EVENT_SIGNATURE = - 0x3684250ce1e33b790ed973c23080f312db0adb21a6d98c61a5c9ff99e4babc17; - - /// @notice Precomputed {ProxyOwnerChanged} event signature - /// @dev keccak256(bytes("ProxyOwnerChanged(address,address)")) - uint256 private constant PROXY_OWNER_CHANGED_EVENT_SIGNATURE = - 0x1b185f8166e5b540f041c2132c66d6c691b0674cd3a95ccc9592a43dd64ad6e2; - - /// @notice Precomputed {ProxyRevoked} event signature - /// @dev keccak256(bytes("ProxyRevoked(address)")) - uint256 private constant PROXY_REVOKED_EVENT_SIGNATURE = - 0x4b0f58242c231a580ee42fe1dd7389c8e7520590afe33c21809305a6014703a1; - - /// @notice Precomputed seed for generating proxy implementation storage slots - /// @dev bytes4(keccak256(bytes("PROXY_IMPLEMENTATION_SLOT"))) - uint256 private constant PROXY_IMPLEMENTATION_SLOT_SEED = 0xa1337b4d; - - /// @notice Precomputed seed for generating proxy owner storage slots - /// @dev bytes4(keccak256(bytes("PROXY_OWNER_SLOT"))) - uint256 private constant PROXY_OWNER_SLOT_SEED = 0xc12fa8d6; - - uint256 private immutable SELF = uint256(uint160(address(this))); - - /// @inheritdoc IProxyForge - function deploy(address implementation, address owner) external payable returns (address proxy) { - return _deploy(implementation, owner, bytes32(0), false, _emptyData()); - } - - /// @inheritdoc IProxyForge - function deployAndCall( - address implementation, - address owner, - bytes calldata data - ) external payable returns (address proxy) { - return _deploy(implementation, owner, bytes32(0), false, data); - } - - /// @inheritdoc IProxyForge - function deployDeterministic( - address implementation, - address owner, - bytes32 salt - ) external payable returns (address proxy) { - return _deploy(implementation, owner, salt, true, _emptyData()); - } - - /// @inheritdoc IProxyForge - function deployDeterministicAndCall( - address implementation, - address owner, - bytes32 salt, - bytes calldata data - ) external payable returns (address proxy) { - return _deploy(implementation, owner, salt, true, data); - } - - /// @dev Internal function that handles all deployment variants - function _deploy( - address implementation, - address owner, - bytes32 salt, - bool isDeterministic, - bytes calldata data - ) internal returns (address proxy) { - assembly ("memory-safe") { - // Verify initial owner is not zero address - if iszero(shl(0x60, owner)) { - mstore(0x00, 0x074b52c9) // InvalidProxyOwner() - revert(0x1c, 0x04) - } - - if isDeterministic { - // Verify first 20 bytes of submitted salt match either caller or zero address - if iszero(or(iszero(shr(0x60, salt)), eq(shr(0x60, salt), caller()))) { - mstore(0x00, 0x81e69d9b) // InvalidSalt() - revert(0x1c, 0x04) - } - } - } - - // Encode constructor parameters - bytes memory parameters = abi.encode(implementation, SELF, data); - - // Concatenate creation code with encoded parameters to assemble initialization code - bytes memory initCode = bytes.concat(type(ForgeProxy).creationCode, parameters); - - // Deploy {ForgeProxy} contract - proxy = isDeterministic ? CreateX.create2(initCode, salt, msg.value) : CreateX.create(initCode, msg.value); - - assembly ("memory-safe") { - // Emit {ProxyDeployed} event - log4(codesize(), 0x00, PROXY_DEPLOYED_EVENT_SIGNATURE, proxy, owner, salt) - - // Compute proxy implementation storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) - // Store initial implementation in designated slot - sstore(keccak256(0x00, 0x20), implementation) - // emit {ProxyUpgraded} event - log3(codesize(), 0x00, PROXY_UPGRADED_EVENT_SIGNATURE, proxy, implementation) - - // Compute proxy owner storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) - // Store initial owner in designated slot - sstore(keccak256(0x00, 0x20), owner) - // emit {ProxyOwnerChanged} event - log3(codesize(), 0x00, PROXY_OWNER_CHANGED_EVENT_SIGNATURE, proxy, owner) - } - } - - /// @inheritdoc IProxyForge - function upgrade(address proxy, address implementation) external payable { - _upgradeAndCall(adminOf(proxy), proxy, implementation, _emptyData()); - } - - /// @inheritdoc IProxyForge - function upgradeAndCall(address proxy, address implementation, bytes calldata data) external payable { - _upgradeAndCall(adminOf(proxy), proxy, implementation, data); - } - - /// @dev Internal function to perform proxy upgrade with optional initialization data - function _upgradeAndCall(address admin, address proxy, address implementation, bytes calldata data) internal { - assembly ("memory-safe") { - // Compute proxy owner storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) - - // Verify caller is owner of proxy - if iszero(eq(sload(keccak256(0x00, 0x20)), caller())) { - mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) - mstore(0x20, caller()) - revert(0x1c, 0x24) - } - - // Compute proxy implementation storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) - let slot := keccak256(0x00, 0x20) - - // Verify new implementation is different from current implementation - if eq(sload(slot), implementation) { - mstore(0x00, 0xdcd488e3) // InvalidProxyImplementation() - revert(0x1c, 0x04) - } - - // Cache free memory pointer to construct upgrade call data - let ptr := mload(0x40) - // Store function selector - mstore(ptr, 0x9623609d) // upgradeAndCall(address,address,bytes) - // Store proxy address - mstore(add(ptr, 0x20), proxy) - // Store implementation address - mstore(add(ptr, 0x40), implementation) - // Store offset to initialization data - mstore(add(ptr, 0x60), 0x60) - // Store initialization data length - mstore(add(ptr, 0x80), data.length) - // Copy initialization data to memory - calldatacopy(add(ptr, 0xa0), data.offset, data.length) - - // Execute call to proxy admin - if iszero(call(gas(), admin, callvalue(), add(ptr, 0x1c), add(data.length, 0xa4), codesize(), 0x00)) { - if iszero(returndatasize()) { - mstore(0x00, 0x55299b49) // UpgradeFailed() - revert(0x1c, 0x04) - } - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) - } - - // Store new implementation in designated slot - sstore(slot, implementation) - // emit {ProxyUpgraded} event - log3(codesize(), 0x00, PROXY_UPGRADED_EVENT_SIGNATURE, proxy, implementation) - } - } - - /// @inheritdoc IProxyForge - function revoke(address proxy) external payable { - _revoke(adminOf(proxy), proxy); - } - - /// @dev Internal function to handle proxy revocation logic - function _revoke(address admin, address proxy) internal { - assembly ("memory-safe") { - // Compute proxy owner storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) - let slot := keccak256(0x00, 0x20) - - // Verify caller is owner of proxy - if iszero(eq(sload(slot), caller())) { - mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) - mstore(0x20, caller()) - revert(0x1c, 0x24) - } - - // Cache free memory pointer - let ptr := mload(0x40) - // Store function selector - mstore(ptr, 0xf2fde38b) // transferOwnership(address) - // Store new owner address - mstore(add(ptr, 0x20), caller()) - - // Execute call to proxy admin - if iszero(call(gas(), admin, 0x00, add(ptr, 0x1c), 0x24, codesize(), 0x00)) { - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) - } - - // Clear proxy owner storage slot - sstore(slot, 0x00) - - // Compute proxy implementation storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) - // Clear proxy implementation storage slot - sstore(keccak256(0x00, 0x20), 0x00) - - // emit {ProxyRevoked} event - log2(codesize(), 0x00, PROXY_REVOKED_EVENT_SIGNATURE, proxy) - } - } - - /// @inheritdoc IProxyForge - function changeOwner(address proxy, address owner) external payable { - assembly ("memory-safe") { - // Compute proxy owner storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) - let slot := keccak256(0x00, 0x20) - - // Verify caller is owner of proxy - if iszero(eq(sload(slot), caller())) { - mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) - mstore(0x20, caller()) - revert(0x1c, 0x24) - } - - // Verify new owner is not zero address - if iszero(shl(0x60, owner)) { - mstore(0x00, 0x074b52c9) // InvalidProxyOwner() - revert(0x1c, 0x04) - } - - // Store new owner in designated slot - sstore(slot, owner) - // emit {ProxyOwnerChanged} event - log3(codesize(), 0x00, PROXY_OWNER_CHANGED_EVENT_SIGNATURE, proxy, owner) - } - } - - /// @inheritdoc IProxyForge - function adminOf(address proxy) public pure returns (address admin) { - return CreateX.computeCreateAddress(proxy, uint256(1)); - } - - /// @inheritdoc IProxyForge - function implementationOf(address proxy) external view returns (address implementation) { - assembly ("memory-safe") { - // Compute proxy implementation storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) - // Hash the 32-byte packed data - implementation := sload(keccak256(0x00, 0x20)) - } - } - - /// @inheritdoc IProxyForge - function ownerOf(address proxy) external view returns (address owner) { - assembly ("memory-safe") { - // Compute proxy owner storage slot - mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) - // Hash the 32-byte packed data - owner := sload(keccak256(0x00, 0x20)) - } - } - - /// @inheritdoc IProxyForge - function computeProxyAddress(uint256 nonce) external view returns (address proxy) { - return CreateX.computeCreateAddress(nonce); - } - - /// @inheritdoc IProxyForge - function computeProxyAddress( - address implementation, - bytes32 salt, - bytes calldata data - ) external view returns (address proxy) { - // Prepare the same initialization code that would be used in actual deployment - bytes memory parameters = abi.encode(implementation, SELF, data); - bytes memory initCode = bytes.concat(type(ForgeProxy).creationCode, parameters); - return CreateX.computeCreate2Address(keccak256(initCode), salt); - } - - /// @dev Returns empty bytes calldata for functions that don't need initialization - function _emptyData() private pure returns (bytes calldata data) { - assembly ("memory-safe") { - data.length := 0x00 - } - } + /// @notice Precomputed {ProxyDeployed} event signature + /// @dev keccak256(bytes("ProxyDeployed(address,address,bytes32)")) + uint256 private constant PROXY_DEPLOYED_EVENT_SIGNATURE = + 0xd283ed05905c0eb69fe3ef042c6ad706d8d9c75b138624098de540fa2c011a05; + + /// @notice Precomputed {ProxyUpgraded} event signature + /// @dev keccak256(bytes("ProxyUpgraded(address,address)")) + uint256 private constant PROXY_UPGRADED_EVENT_SIGNATURE = + 0x3684250ce1e33b790ed973c23080f312db0adb21a6d98c61a5c9ff99e4babc17; + + /// @notice Precomputed {ProxyOwnerChanged} event signature + /// @dev keccak256(bytes("ProxyOwnerChanged(address,address)")) + uint256 private constant PROXY_OWNER_CHANGED_EVENT_SIGNATURE = + 0x1b185f8166e5b540f041c2132c66d6c691b0674cd3a95ccc9592a43dd64ad6e2; + + /// @notice Precomputed {ProxyRevoked} event signature + /// @dev keccak256(bytes("ProxyRevoked(address)")) + uint256 private constant PROXY_REVOKED_EVENT_SIGNATURE = + 0x4b0f58242c231a580ee42fe1dd7389c8e7520590afe33c21809305a6014703a1; + + /// @notice Precomputed seed for generating proxy implementation storage slots + /// @dev bytes4(keccak256(bytes("PROXY_IMPLEMENTATION_SLOT"))) + uint256 private constant PROXY_IMPLEMENTATION_SLOT_SEED = 0xa1337b4d; + + /// @notice Precomputed seed for generating proxy owner storage slots + /// @dev bytes4(keccak256(bytes("PROXY_OWNER_SLOT"))) + uint256 private constant PROXY_OWNER_SLOT_SEED = 0xc12fa8d6; + + uint256 private immutable SELF = uint256(uint160(address(this))); + + /// @inheritdoc IProxyForge + function deploy(address implementation, address owner) external payable returns (address proxy) { + return _deploy(implementation, owner, bytes32(0), false, _emptyData()); + } + + /// @inheritdoc IProxyForge + function deployAndCall(address implementation, address owner, bytes calldata data) + external + payable + returns (address proxy) + { + return _deploy(implementation, owner, bytes32(0), false, data); + } + + /// @inheritdoc IProxyForge + function deployDeterministic(address implementation, address owner, bytes32 salt) + external + payable + returns (address proxy) + { + return _deploy(implementation, owner, salt, true, _emptyData()); + } + + /// @inheritdoc IProxyForge + function deployDeterministicAndCall(address implementation, address owner, bytes32 salt, bytes calldata data) + external + payable + returns (address proxy) + { + return _deploy(implementation, owner, salt, true, data); + } + + /// @dev Internal function that handles all deployment variants + function _deploy(address implementation, address owner, bytes32 salt, bool isDeterministic, bytes calldata data) + internal + returns (address proxy) + { + assembly ("memory-safe") { + // Verify initial owner is not zero address + if iszero(shl(0x60, owner)) { + mstore(0x00, 0x074b52c9) // InvalidProxyOwner() + revert(0x1c, 0x04) + } + + if isDeterministic { + // Verify first 20 bytes of submitted salt match either caller or zero address + if iszero(or(iszero(shr(0x60, salt)), eq(shr(0x60, salt), caller()))) { + mstore(0x00, 0x81e69d9b) // InvalidSalt() + revert(0x1c, 0x04) + } + } + } + + // Encode constructor parameters + bytes memory parameters = abi.encode(implementation, SELF, data); + + // Concatenate creation code with encoded parameters to assemble initialization code + bytes memory initCode = bytes.concat(type(ForgeProxy).creationCode, parameters); + + // Deploy {ForgeProxy} contract + proxy = isDeterministic ? CreateX.create2(initCode, salt, msg.value) : CreateX.create(initCode, msg.value); + + assembly ("memory-safe") { + // Emit {ProxyDeployed} event + log4(codesize(), 0x00, PROXY_DEPLOYED_EVENT_SIGNATURE, proxy, owner, salt) + + // Compute proxy implementation storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) + // Store initial implementation in designated slot + sstore(keccak256(0x00, 0x20), implementation) + // emit {ProxyUpgraded} event + log3(codesize(), 0x00, PROXY_UPGRADED_EVENT_SIGNATURE, proxy, implementation) + + // Compute proxy owner storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) + // Store initial owner in designated slot + sstore(keccak256(0x00, 0x20), owner) + // emit {ProxyOwnerChanged} event + log3(codesize(), 0x00, PROXY_OWNER_CHANGED_EVENT_SIGNATURE, proxy, owner) + } + } + + /// @inheritdoc IProxyForge + function upgrade(address proxy, address implementation) external payable { + _upgradeAndCall(adminOf(proxy), proxy, implementation, _emptyData()); + } + + /// @inheritdoc IProxyForge + function upgradeAndCall(address proxy, address implementation, bytes calldata data) external payable { + _upgradeAndCall(adminOf(proxy), proxy, implementation, data); + } + + /// @dev Internal function to perform proxy upgrade with optional initialization data + function _upgradeAndCall(address admin, address proxy, address implementation, bytes calldata data) internal { + assembly ("memory-safe") { + // Compute proxy owner storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) + + // Verify caller is owner of proxy + if iszero(eq(sload(keccak256(0x00, 0x20)), caller())) { + mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) + mstore(0x20, caller()) + revert(0x1c, 0x24) + } + + // Compute proxy implementation storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) + let slot := keccak256(0x00, 0x20) + + // Verify new implementation is different from current implementation + if eq(sload(slot), implementation) { + mstore(0x00, 0xdcd488e3) // InvalidProxyImplementation() + revert(0x1c, 0x04) + } + + // Cache free memory pointer to construct upgrade call data + let ptr := mload(0x40) + // Store function selector + mstore(ptr, 0x9623609d) // upgradeAndCall(address,address,bytes) + // Store proxy address + mstore(add(ptr, 0x20), proxy) + // Store implementation address + mstore(add(ptr, 0x40), implementation) + // Store offset to initialization data + mstore(add(ptr, 0x60), 0x60) + // Store initialization data length + mstore(add(ptr, 0x80), data.length) + // Copy initialization data to memory + calldatacopy(add(ptr, 0xa0), data.offset, data.length) + + // Execute call to proxy admin + if iszero(call(gas(), admin, callvalue(), add(ptr, 0x1c), add(data.length, 0xa4), codesize(), 0x00)) { + if iszero(returndatasize()) { + mstore(0x00, 0x55299b49) // UpgradeFailed() + revert(0x1c, 0x04) + } + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + + // Store new implementation in designated slot + sstore(slot, implementation) + // emit {ProxyUpgraded} event + log3(codesize(), 0x00, PROXY_UPGRADED_EVENT_SIGNATURE, proxy, implementation) + } + } + + /// @inheritdoc IProxyForge + function revoke(address proxy) external payable { + _revoke(adminOf(proxy), proxy); + } + + /// @dev Internal function to handle proxy revocation logic + function _revoke(address admin, address proxy) internal { + assembly ("memory-safe") { + // Compute proxy owner storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) + let slot := keccak256(0x00, 0x20) + + // Verify caller is owner of proxy + if iszero(eq(sload(slot), caller())) { + mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) + mstore(0x20, caller()) + revert(0x1c, 0x24) + } + + // Cache free memory pointer + let ptr := mload(0x40) + // Store function selector + mstore(ptr, 0xf2fde38b) // transferOwnership(address) + // Store new owner address + mstore(add(ptr, 0x20), caller()) + + // Execute call to proxy admin + if iszero(call(gas(), admin, 0x00, add(ptr, 0x1c), 0x24, codesize(), 0x00)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + + // Clear proxy owner storage slot + sstore(slot, 0x00) + + // Compute proxy implementation storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) + // Clear proxy implementation storage slot + sstore(keccak256(0x00, 0x20), 0x00) + + // emit {ProxyRevoked} event + log2(codesize(), 0x00, PROXY_REVOKED_EVENT_SIGNATURE, proxy) + } + } + + /// @inheritdoc IProxyForge + function changeOwner(address proxy, address owner) external payable { + assembly ("memory-safe") { + // Compute proxy owner storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) + let slot := keccak256(0x00, 0x20) + + // Verify caller is owner of proxy + if iszero(eq(sload(slot), caller())) { + mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) + mstore(0x20, caller()) + revert(0x1c, 0x24) + } + + // Verify new owner is not zero address + if iszero(shl(0x60, owner)) { + mstore(0x00, 0x074b52c9) // InvalidProxyOwner() + revert(0x1c, 0x04) + } + + // Store new owner in designated slot + sstore(slot, owner) + // emit {ProxyOwnerChanged} event + log3(codesize(), 0x00, PROXY_OWNER_CHANGED_EVENT_SIGNATURE, proxy, owner) + } + } + + /// @inheritdoc IProxyForge + function adminOf(address proxy) public pure returns (address admin) { + return CreateX.computeCreateAddress(proxy, uint256(1)); + } + + /// @inheritdoc IProxyForge + function implementationOf(address proxy) external view returns (address implementation) { + assembly ("memory-safe") { + // Compute proxy implementation storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_IMPLEMENTATION_SLOT_SEED)) + // Hash the 32-byte packed data + implementation := sload(keccak256(0x00, 0x20)) + } + } + + /// @inheritdoc IProxyForge + function ownerOf(address proxy) external view returns (address owner) { + assembly ("memory-safe") { + // Compute proxy owner storage slot + mstore(0x00, or(shl(0x60, proxy), PROXY_OWNER_SLOT_SEED)) + // Hash the 32-byte packed data + owner := sload(keccak256(0x00, 0x20)) + } + } + + /// @inheritdoc IProxyForge + function computeProxyAddress(uint256 nonce) external view returns (address proxy) { + return CreateX.computeCreateAddress(nonce); + } + + /// @inheritdoc IProxyForge + function computeProxyAddress(address implementation, bytes32 salt, bytes calldata data) + external + view + returns (address proxy) + { + // Prepare the same initialization code that would be used in actual deployment + bytes memory parameters = abi.encode(implementation, SELF, data); + bytes memory initCode = bytes.concat(type(ForgeProxy).creationCode, parameters); + return CreateX.computeCreate2Address(keccak256(initCode), salt); + } + + /// @dev Returns empty bytes calldata for functions that don't need initialization + function _emptyData() private pure returns (bytes calldata data) { + assembly ("memory-safe") { + data.length := 0x00 + } + } } diff --git a/src/interfaces/IForgeProxy.sol b/src/interfaces/IForgeProxy.sol index 44d9c99..025cfc2 100644 --- a/src/interfaces/IForgeProxy.sol +++ b/src/interfaces/IForgeProxy.sol @@ -3,5 +3,5 @@ pragma solidity ^0.8.30; /// @title IForgeProxy interface IForgeProxy { - function upgradeToAndCall(address implementation, bytes calldata data) external payable; + function upgradeToAndCall(address implementation, bytes calldata data) external payable; } diff --git a/src/interfaces/IForgeProxyAdmin.sol b/src/interfaces/IForgeProxyAdmin.sol index a510108..b8e6cec 100644 --- a/src/interfaces/IForgeProxyAdmin.sol +++ b/src/interfaces/IForgeProxyAdmin.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.30; /// @title IForgeProxyAdmin interface IForgeProxyAdmin { - function UPGRADE_INTERFACE_VERSION() external view returns (string memory); + function UPGRADE_INTERFACE_VERSION() external view returns (string memory); - function owner() external view returns (address); + function owner() external view returns (address); - function transferOwnership(address newOwner) external; + function transferOwnership(address newOwner) external; - function upgradeAndCall(address proxy, address implementation, bytes calldata data) external payable; + function upgradeAndCall(address proxy, address implementation, bytes calldata data) external payable; } diff --git a/src/interfaces/IProxyForge.sol b/src/interfaces/IProxyForge.sol index 06abee5..77c9560 100644 --- a/src/interfaces/IProxyForge.sol +++ b/src/interfaces/IProxyForge.sol @@ -4,134 +4,129 @@ pragma solidity ^0.8.30; /// @title IProxyForge /// @notice Interface for a factory contract that manages deployment, upgrades, and ownership of upgradeable proxy contracts interface IProxyForge { - /// @notice Thrown when an invalid proxy address is provided - error InvalidProxy(); - - /// @notice Thrown when an invalid implementation address is provided - error InvalidProxyImplementation(); - - /// @notice Thrown when an invalid proxy owner address is provided - error InvalidProxyOwner(); - - /// @notice Thrown when an invalid salt is provided for deterministic deployment - error InvalidSalt(); - - /// @notice Thrown when unauthorized account attempts restricted operation - error UnauthorizedAccount(address account); - - /// @notice Thrown when proxy upgrade operation fails - error UpgradeFailed(); - - /// @notice Emitted when a new proxy is deployed - /// @param proxy Address of the deployed proxy contract - /// @param owner Address of the proxy owner who can manage the proxy - /// @param salt A unique 32-byte value used in deterministic address derivation (zero for CREATE) - event ProxyDeployed(address indexed proxy, address indexed owner, bytes32 indexed salt); - - /// @notice Emitted when a proxy's implementation is upgraded - /// @param proxy Address of the proxy whose implementation was upgraded - /// @param implementation Address of the new implementation contract - event ProxyUpgraded(address indexed proxy, address indexed implementation); - - /// @notice Emitted when a proxy's owner is changed - /// @param proxy Address of the proxy whose owner was changed - /// @param owner Address of the new owner - event ProxyOwnerChanged(address indexed proxy, address indexed owner); - - /// @notice Emitted when a proxy is revoked (ownership transferred to its owner) - /// @param proxy Address of the proxy that was revoked - event ProxyRevoked(address indexed proxy); - - /// @notice Deploys a new proxy using CREATE opcode - /// @param implementation Address of the initial implementation contract - /// @param owner Address designated as the owner of the proxy on this factory - /// @return proxy Address of the deployed proxy contract - function deploy(address implementation, address owner) external payable returns (address proxy); - - /// @notice Deploys a new proxy with initialization using CREATE opcode - /// @param implementation Address of the initial implementation contract - /// @param owner Address designated as the owner of the proxy on this factory - /// @param data Optional initialization data to call on the implementation (can be empty bytes) - /// @return proxy Address of the deployed proxy contract - function deployAndCall( - address implementation, - address owner, - bytes calldata data - ) external payable returns (address proxy); - - /// @notice Deploys a new proxy using CREATE2 opcode - /// @param implementation Address of the initial implementation contract - /// @param owner Address designated as the owner of the proxy on this factory - /// @param salt A unique 32-byte value used in deterministic address derivation - /// @return proxy Address of the deployed proxy contract - function deployDeterministic( - address implementation, - address owner, - bytes32 salt - ) external payable returns (address proxy); - - /// @notice Deploys a new proxy with initialization using CREATE2 opcode - /// @param implementation Address of the initial implementation contract - /// @param owner Address designated as the owner of the proxy on this factory - /// @param salt A unique 32-byte value used in deterministic address derivation - /// @param data Optional initialization data to call on the implementation (can be empty bytes) - /// @return proxy Address of the deployed proxy contract - function deployDeterministicAndCall( - address implementation, - address owner, - bytes32 salt, - bytes calldata data - ) external payable returns (address proxy); - - /// @notice Upgrades a proxy to a new implementation without initialization - /// @param proxy Address of the proxy to perform upgrade - /// @param implementation Address of the new implementation contract - function upgrade(address proxy, address implementation) external payable; - - /// @notice Upgrades a proxy to a new implementation and optionally calls initialization function - /// @param proxy Address of the proxy to perform upgrade - /// @param implementation Address of the new implementation contract - /// @param data Optional initialization data to call on the new implementation (can be empty bytes) - function upgradeAndCall(address proxy, address implementation, bytes calldata data) external payable; - - /// @notice Revokes factory management of a proxy, transferring admin contract ownership to caller - /// @dev After revocation, the proxy can no longer be managed through this factory - /// @param proxy Address of the proxy to revoke - function revoke(address proxy) external payable; - - /// @notice Transfers ownership of a proxy to a new owner - /// @param proxy Address of the proxy whose ownership will be transferred - /// @param owner Address of the new owner - function changeOwner(address proxy, address owner) external payable; - - /// @notice Returns the admin address for a specific proxy - /// @param proxy Address of the proxy to query - /// @return admin Address of the {ProxyAdmin} contract managing the proxy - function adminOf(address proxy) external view returns (address admin); - - /// @notice Returns the current implementation address for a specific proxy - /// @param proxy Address of the proxy to query - /// @return implementation Address of the current implementation contract - function implementationOf(address proxy) external view returns (address implementation); - - /// @notice Returns the current owner address for a specific proxy - /// @param proxy Address of the proxy to query - /// @return owner Address of the current proxy owner - function ownerOf(address proxy) external view returns (address owner); - - /// @notice Computes the predicted address of a proxy deployed using CREATE opcode - /// @param nonce Nonce value at the time of the deployment (must be < 2^64 - 1 per EIP-2681) - /// @return proxy Predicted proxy address - function computeProxyAddress(uint256 nonce) external view returns (address proxy); - - /// @notice Computes the predicted address of a proxy deployed using CREATE2 opcode - /// @param implementation Address of the implementation contract - /// @param salt A unique 32-byte value used in deterministic address derivation - /// @param data Optional initialization data for the proxy (can be empty bytes) - /// @return proxy Predicted proxy address - function computeProxyAddress( - address implementation, - bytes32 salt, - bytes calldata data - ) external view returns (address proxy); + /// @notice Thrown when an invalid proxy address is provided + error InvalidProxy(); + + /// @notice Thrown when an invalid implementation address is provided + error InvalidProxyImplementation(); + + /// @notice Thrown when an invalid proxy owner address is provided + error InvalidProxyOwner(); + + /// @notice Thrown when an invalid salt is provided for deterministic deployment + error InvalidSalt(); + + /// @notice Thrown when unauthorized account attempts restricted operation + error UnauthorizedAccount(address account); + + /// @notice Thrown when proxy upgrade operation fails + error UpgradeFailed(); + + /// @notice Emitted when a new proxy is deployed + /// @param proxy Address of the deployed proxy contract + /// @param owner Address of the proxy owner who can manage the proxy + /// @param salt A unique 32-byte value used in deterministic address derivation (zero for CREATE) + event ProxyDeployed(address indexed proxy, address indexed owner, bytes32 indexed salt); + + /// @notice Emitted when a proxy's implementation is upgraded + /// @param proxy Address of the proxy whose implementation was upgraded + /// @param implementation Address of the new implementation contract + event ProxyUpgraded(address indexed proxy, address indexed implementation); + + /// @notice Emitted when a proxy's owner is changed + /// @param proxy Address of the proxy whose owner was changed + /// @param owner Address of the new owner + event ProxyOwnerChanged(address indexed proxy, address indexed owner); + + /// @notice Emitted when a proxy is revoked (ownership transferred to its owner) + /// @param proxy Address of the proxy that was revoked + event ProxyRevoked(address indexed proxy); + + /// @notice Deploys a new proxy using CREATE opcode + /// @param implementation Address of the initial implementation contract + /// @param owner Address designated as the owner of the proxy on this factory + /// @return proxy Address of the deployed proxy contract + function deploy(address implementation, address owner) external payable returns (address proxy); + + /// @notice Deploys a new proxy with initialization using CREATE opcode + /// @param implementation Address of the initial implementation contract + /// @param owner Address designated as the owner of the proxy on this factory + /// @param data Optional initialization data to call on the implementation (can be empty bytes) + /// @return proxy Address of the deployed proxy contract + function deployAndCall(address implementation, address owner, bytes calldata data) + external + payable + returns (address proxy); + + /// @notice Deploys a new proxy using CREATE2 opcode + /// @param implementation Address of the initial implementation contract + /// @param owner Address designated as the owner of the proxy on this factory + /// @param salt A unique 32-byte value used in deterministic address derivation + /// @return proxy Address of the deployed proxy contract + function deployDeterministic(address implementation, address owner, bytes32 salt) + external + payable + returns (address proxy); + + /// @notice Deploys a new proxy with initialization using CREATE2 opcode + /// @param implementation Address of the initial implementation contract + /// @param owner Address designated as the owner of the proxy on this factory + /// @param salt A unique 32-byte value used in deterministic address derivation + /// @param data Optional initialization data to call on the implementation (can be empty bytes) + /// @return proxy Address of the deployed proxy contract + function deployDeterministicAndCall(address implementation, address owner, bytes32 salt, bytes calldata data) + external + payable + returns (address proxy); + + /// @notice Upgrades a proxy to a new implementation without initialization + /// @param proxy Address of the proxy to perform upgrade + /// @param implementation Address of the new implementation contract + function upgrade(address proxy, address implementation) external payable; + + /// @notice Upgrades a proxy to a new implementation and optionally calls initialization function + /// @param proxy Address of the proxy to perform upgrade + /// @param implementation Address of the new implementation contract + /// @param data Optional initialization data to call on the new implementation (can be empty bytes) + function upgradeAndCall(address proxy, address implementation, bytes calldata data) external payable; + + /// @notice Revokes factory management of a proxy, transferring admin contract ownership to caller + /// @dev After revocation, the proxy can no longer be managed through this factory + /// @param proxy Address of the proxy to revoke + function revoke(address proxy) external payable; + + /// @notice Transfers ownership of a proxy to a new owner + /// @param proxy Address of the proxy whose ownership will be transferred + /// @param owner Address of the new owner + function changeOwner(address proxy, address owner) external payable; + + /// @notice Returns the admin address for a specific proxy + /// @param proxy Address of the proxy to query + /// @return admin Address of the {ProxyAdmin} contract managing the proxy + function adminOf(address proxy) external view returns (address admin); + + /// @notice Returns the current implementation address for a specific proxy + /// @param proxy Address of the proxy to query + /// @return implementation Address of the current implementation contract + function implementationOf(address proxy) external view returns (address implementation); + + /// @notice Returns the current owner address for a specific proxy + /// @param proxy Address of the proxy to query + /// @return owner Address of the current proxy owner + function ownerOf(address proxy) external view returns (address owner); + + /// @notice Computes the predicted address of a proxy deployed using CREATE opcode + /// @param nonce Nonce value at the time of the deployment (must be < 2^64 - 1 per EIP-2681) + /// @return proxy Predicted proxy address + function computeProxyAddress(uint256 nonce) external view returns (address proxy); + + /// @notice Computes the predicted address of a proxy deployed using CREATE2 opcode + /// @param implementation Address of the implementation contract + /// @param salt A unique 32-byte value used in deterministic address derivation + /// @param data Optional initialization data for the proxy (can be empty bytes) + /// @return proxy Predicted proxy address + function computeProxyAddress(address implementation, bytes32 salt, bytes calldata data) + external + view + returns (address proxy); } diff --git a/src/proxy/ForgeProxy.sol b/src/proxy/ForgeProxy.sol index 8721247..f986729 100644 --- a/src/proxy/ForgeProxy.sol +++ b/src/proxy/ForgeProxy.sol @@ -6,179 +6,175 @@ import {ForgeProxyAdmin} from "./ForgeProxyAdmin.sol"; /// @title ForgeProxy /// @notice Transparent upgradeable proxy implementation with radical gas optimizations contract ForgeProxy { - /// @notice Thrown when invalid admin address is provided - error InvalidAdmin(); - - /// @notice Thrown when invalid implementation address is provided - error InvalidImplementation(); - - /// @notice Thrown when Ether is sent to upgrade function with empty data - error NonPayable(); - - /// @notice Thrown when admin attempts to access implementation functions - error ProxyDeniedAdminAccess(); - - /// @notice Emitted when the proxy's admin is changed - event AdminChanged(address previousAdmin, address newAdmin); - - /// @notice Emitted when the proxy's implementation is upgraded - event Upgraded(address indexed implementation); - - /// @notice Precomputed keccak256 hash for {AdminChanged} event signature - /// @dev keccak256(bytes("AdminChanged(address,address)")) - uint256 private constant ADMIN_CHANGED_EVENT_SIGNATURE = - 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f; - - /// @notice Precomputed keccak256 hash for {Upgraded} event signature - /// @dev keccak256(bytes("Upgraded(address)")) - uint256 private constant UPGRADED_EVENT_SIGNATURE = - 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b; - - /// @notice Precomputed storage slot for admin using ERC-1967 standard - /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.admin"))) - 1) - uint256 private constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - - /// @notice Precomputed storage slot for implementation using ERC-1967 standard - /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.implementation"))) - 1) - uint256 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /// @dev An immutable address for admin for gas-efficient access control - uint256 private immutable _admin; - - /// @notice Initializes an upgradeable proxy with automatic {ForgeProxyAdmin} creation - /// @param implementation Address of initial implementation - /// @param initialOwner Address designated as initial owner of its admin contract - /// @param data Optional initialization data to call on initial implementation (can be empty bytes) - constructor(address implementation, address initialOwner, bytes memory data) payable { - // Concatenate creation code with encoded constructor parameters to assemble initialization code - bytes memory initCode = bytes.concat(type(ForgeProxyAdmin).creationCode, abi.encode(initialOwner)); - - uint256 admin; - - assembly ("memory-safe") { - // Verify initial implementation address contains code - if iszero(extcodesize(implementation)) { - mstore(0x00, 0x68155f9a) // InvalidImplementation() - revert(0x1c, 0x04) - } - - // Store implementation in designated slot - sstore(IMPLEMENTATION_SLOT, implementation) - - // Emit {Upgraded} event - log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, implementation) - - // Handle optional initialization data - switch mload(data) - case 0x00 { - // Ensure no Ether sent - if callvalue() { - mstore(0x00, 0x6fb1b0e9) // NonPayable() - revert(0x1c, 0x04) - } - } - default { - // Execute delegatecall to initial implementation - if iszero(delegatecall(gas(), implementation, add(data, 0x20), mload(data), codesize(), 0x00)) { - returndatacopy(0x00, 0x00, returndatasize()) - revert(0x00, returndatasize()) - } - } - - // Deploy {ForgeProxyAdmin} contract - admin := create(0x00, add(initCode, 0x20), mload(initCode)) - - // Verify deployed admin is not zero address - if iszero(shl(0x60, admin)) { - returndatacopy(0x00, 0x00, returndatasize()) - revert(0x00, returndatasize()) - } - - // Store admin in designated slot - sstore(ADMIN_SLOT, admin) - - // Emit {AdminChanged} event - mstore(0x20, admin) - log1(0x00, 0x40, ADMIN_CHANGED_EVENT_SIGNATURE) - } - - _admin = admin; - } - - fallback() external payable { - uint256 admin = _admin; - assembly ("memory-safe") { - switch iszero(eq(admin, caller())) - // Path 1: Admin Access - Upgrade operation - case 0x00 { - // upgradeToAndCall(address,bytes) calldata structure: - // 0x00-0x03: function selector (0x4f1ef286) (4 bytes) - // 0x04-0x23: implementation address (32 bytes) - // 0x24-0x43: offset to bytes data (0x40) (32 bytes) - // 0x44-0x63: length of bytes data (32 bytes) - // 0x64+ : bytes initialization data (variable length) - - // Extract function selector from calldata and verify it's permitted for admin - if iszero(eq(shr(0xe0, calldataload(0x00)), 0x4f1ef286)) { - mstore(0x00, 0xd2b576ec) // ProxyDeniedAdminAccess() - revert(0x1c, 0x04) - } - - // Extract new implementation address from calldata - let implementation := shr(0x60, shl(0x60, calldataload(0x04))) - - // Verify new implementation address contains code - if iszero(extcodesize(implementation)) { - mstore(0x00, 0x68155f9a) // InvalidImplementation() - revert(0x1c, 0x04) - } - - // Store new implementation in designated slot - sstore(IMPLEMENTATION_SLOT, implementation) - - // Emit {Upgraded} event - log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, implementation) - - // Handle optional initialization call - switch calldataload(0x44) - case 0x00 { - // Ensure no Ether sent - if callvalue() { - mstore(0x00, 0x6fb1b0e9) // NonPayable() - revert(0x1c, 0x04) - } - } - default { - // Copy initialization data to memory - calldatacopy(0x00, 0x64, calldataload(0x44)) - - // Execute delegatecall to new implementation - if iszero(delegatecall(gas(), implementation, 0x00, calldataload(0x44), codesize(), 0x00)) { - returndatacopy(0x00, 0x00, returndatasize()) - revert(0x00, returndatasize()) - } - } - } - // Path 2: User Access - Delegate to implementation - default { - // Copy entire call data to memory - calldatacopy(0x00, 0x00, calldatasize()) - - // Execute delegatecall to current implementation - let success := delegatecall(gas(), sload(IMPLEMENTATION_SLOT), 0x00, calldatasize(), codesize(), 0x00) - - // Copy entire return data to memory - returndatacopy(0x00, 0x00, returndatasize()) - - // Handle call result - switch success - case 0x00 { - revert(0x00, returndatasize()) - } - default { - return(0x00, returndatasize()) - } - } - } - } + /// @notice Thrown when invalid admin address is provided + error InvalidAdmin(); + + /// @notice Thrown when invalid implementation address is provided + error InvalidImplementation(); + + /// @notice Thrown when Ether is sent to upgrade function with empty data + error NonPayable(); + + /// @notice Thrown when admin attempts to access implementation functions + error ProxyDeniedAdminAccess(); + + /// @notice Emitted when the proxy's admin is changed + event AdminChanged(address previousAdmin, address newAdmin); + + /// @notice Emitted when the proxy's implementation is upgraded + event Upgraded(address indexed implementation); + + /// @notice Precomputed keccak256 hash for {AdminChanged} event signature + /// @dev keccak256(bytes("AdminChanged(address,address)")) + uint256 private constant ADMIN_CHANGED_EVENT_SIGNATURE = + 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f; + + /// @notice Precomputed keccak256 hash for {Upgraded} event signature + /// @dev keccak256(bytes("Upgraded(address)")) + uint256 private constant UPGRADED_EVENT_SIGNATURE = + 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b; + + /// @notice Precomputed storage slot for admin using ERC-1967 standard + /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.admin"))) - 1) + uint256 private constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /// @notice Precomputed storage slot for implementation using ERC-1967 standard + /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.implementation"))) - 1) + uint256 private constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /// @dev An immutable address for admin for gas-efficient access control + uint256 private immutable _admin; + + /// @notice Initializes an upgradeable proxy with automatic {ForgeProxyAdmin} creation + /// @param implementation Address of initial implementation + /// @param initialOwner Address designated as initial owner of its admin contract + /// @param data Optional initialization data to call on initial implementation (can be empty bytes) + constructor(address implementation, address initialOwner, bytes memory data) payable { + // Concatenate creation code with encoded constructor parameters to assemble initialization code + bytes memory initCode = bytes.concat(type(ForgeProxyAdmin).creationCode, abi.encode(initialOwner)); + + uint256 admin; + + assembly ("memory-safe") { + // Verify initial implementation address contains code + if iszero(extcodesize(implementation)) { + mstore(0x00, 0x68155f9a) // InvalidImplementation() + revert(0x1c, 0x04) + } + + // Store implementation in designated slot + sstore(IMPLEMENTATION_SLOT, implementation) + + // Emit {Upgraded} event + log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, implementation) + + // Handle optional initialization data + switch mload(data) + case 0x00 { + // Ensure no Ether sent + if callvalue() { + mstore(0x00, 0x6fb1b0e9) // NonPayable() + revert(0x1c, 0x04) + } + } + default { + // Execute delegatecall to initial implementation + if iszero(delegatecall(gas(), implementation, add(data, 0x20), mload(data), codesize(), 0x00)) { + returndatacopy(0x00, 0x00, returndatasize()) + revert(0x00, returndatasize()) + } + } + + // Deploy {ForgeProxyAdmin} contract + admin := create(0x00, add(initCode, 0x20), mload(initCode)) + + // Verify deployed admin is not zero address + if iszero(shl(0x60, admin)) { + returndatacopy(0x00, 0x00, returndatasize()) + revert(0x00, returndatasize()) + } + + // Store admin in designated slot + sstore(ADMIN_SLOT, admin) + + // Emit {AdminChanged} event + mstore(0x20, admin) + log1(0x00, 0x40, ADMIN_CHANGED_EVENT_SIGNATURE) + } + + _admin = admin; + } + + fallback() external payable { + uint256 admin = _admin; + assembly ("memory-safe") { + switch iszero(eq(admin, caller())) + // Path 1: Admin Access - Upgrade operation + case 0x00 { + // upgradeToAndCall(address,bytes) calldata structure: + // 0x00-0x03: function selector (0x4f1ef286) (4 bytes) + // 0x04-0x23: implementation address (32 bytes) + // 0x24-0x43: offset to bytes data (0x40) (32 bytes) + // 0x44-0x63: length of bytes data (32 bytes) + // 0x64+ : bytes initialization data (variable length) + + // Extract function selector from calldata and verify it's permitted for admin + if iszero(eq(shr(0xe0, calldataload(0x00)), 0x4f1ef286)) { + mstore(0x00, 0xd2b576ec) // ProxyDeniedAdminAccess() + revert(0x1c, 0x04) + } + + // Extract new implementation address from calldata + let implementation := shr(0x60, shl(0x60, calldataload(0x04))) + + // Verify new implementation address contains code + if iszero(extcodesize(implementation)) { + mstore(0x00, 0x68155f9a) // InvalidImplementation() + revert(0x1c, 0x04) + } + + // Store new implementation in designated slot + sstore(IMPLEMENTATION_SLOT, implementation) + + // Emit {Upgraded} event + log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, implementation) + + // Handle optional initialization call + switch calldataload(0x44) + case 0x00 { + // Ensure no Ether sent + if callvalue() { + mstore(0x00, 0x6fb1b0e9) // NonPayable() + revert(0x1c, 0x04) + } + } + default { + // Copy initialization data to memory + calldatacopy(0x00, 0x64, calldataload(0x44)) + + // Execute delegatecall to new implementation + if iszero(delegatecall(gas(), implementation, 0x00, calldataload(0x44), codesize(), 0x00)) { + returndatacopy(0x00, 0x00, returndatasize()) + revert(0x00, returndatasize()) + } + } + } + // Path 2: User Access - Delegate to implementation + default { + // Copy entire call data to memory + calldatacopy(0x00, 0x00, calldatasize()) + + // Execute delegatecall to current implementation + let success := delegatecall(gas(), sload(IMPLEMENTATION_SLOT), 0x00, calldatasize(), codesize(), 0x00) + + // Copy entire return data to memory + returndatacopy(0x00, 0x00, returndatasize()) + + // Handle call result + switch success + case 0x00 { revert(0x00, returndatasize()) } + default { return(0x00, returndatasize()) } + } + } + } } diff --git a/src/proxy/ForgeProxyAdmin.sol b/src/proxy/ForgeProxyAdmin.sol index 3abbd40..689bf38 100644 --- a/src/proxy/ForgeProxyAdmin.sol +++ b/src/proxy/ForgeProxyAdmin.sol @@ -4,147 +4,147 @@ pragma solidity ^0.8.30; /// @title ForgeProxyAdmin /// @notice Ultra-lightweight admin contract responsible for upgrading {ForgeProxy} instances contract ForgeProxyAdmin { - /// @notice Thrown when calldata length is insufficient for function call - error InvalidCalldataLength(); - - /// @notice Thrown when attempting to set invalid new owner - error InvalidNewOwner(); - - /// @notice Thrown when an unknown function selector is called - error InvalidSelector(); - - /// @notice Thrown when unauthorized account attempts admin operation - error UnauthorizedAccount(address account); - - /// @notice Emitted when ownership is transferred from one account to another - /// @param previousOwner Address of previous owner - /// @param newOwner Address of new owner - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - /// @notice Precomputed keccak256 hash for {OwnershipTransferred} event signature - /// @dev keccak256(bytes("OwnershipTransferred(address,address)")) - uint256 private constant OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE = - 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0; - - /// @notice Precomputed storage slot for owner using ERC-1967 standard - /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxyAdmin.owner"))) - 1) - uint256 private constant OWNER_SLOT = 0x9bc353c4ee8d049c7cb68b79467fc95d9015a8a82334bd0e61ce699e20cb5bd5; - - /// @notice Initializes the contract setting provided address as initial owner - /// @param initialOwner Address designated as initial owner of this contract - constructor(address initialOwner) { - assembly ("memory-safe") { - // Verify initial owner is not zero address - if iszero(shl(0x60, initialOwner)) { - mstore(0x00, 0x54a56786) // InvalidNewOwner() - revert(0x1c, 0x04) - } - - // Store initial owner in designated slot - sstore(OWNER_SLOT, initialOwner) - - // Emit {OwnershipTransferred} event - log3(codesize(), 0x00, OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0x00, initialOwner) - } - } - - fallback() external payable { - assembly ("memory-safe") { - // Ensure minimum calldata length - if lt(calldatasize(), 0x04) { - mstore(0x00, 0xca0ad260) // InvalidCalldataLength() - revert(0x1c, 0x04) - } - - // Extract function selector from calldata - let selector := shr(0xe0, calldataload(0x00)) - - // Stage 1: Permission-free functions (accessible by anyone) - switch selector - // owner() - case 0x8da5cb5b { - mstore(0x00, sload(OWNER_SLOT)) - return(0x00, 0x20) - } - // UPGRADE_INTERFACE_VERSION() - case 0xad3cb1cc { - // Compatible with OpenZeppelin's ProxyAdmin interface version - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/101bbaf1a8e02f95392380586ba0fc5752c4204d/contracts/proxy/transparent/ProxyAdmin.sol#L22 - mstore(0x00, 0x20) // String offset - mstore(0x20, 0x05) // String length - mstore(0x40, hex"352e302e30") // "5.0.0" in hex - return(0x00, 0x60) - } - - // Verify caller is authorized for owner-only functions - if iszero(eq(sload(OWNER_SLOT), caller())) { - mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) - mstore(0x20, caller()) - revert(0x1c, 0x24) - } - - // Stage 2: Owner-only functions (authorization verified above) - switch selector - // upgradeAndCall(address,address,bytes) - case 0x9623609d { - // Calldata Transformation Process: - - // INPUT - upgradeAndCall(address proxy, address implementation, bytes data): - // 0x00-0x03: function selector (0x9623609d) (4 bytes) - // 0x04-0x23: proxy address (32 bytes) - // 0x24-0x43: implementation address (32 bytes) - // 0x44-0x63: offset to bytes data (0x60) (32 bytes) - // 0x64-0x83: length of bytes data (32 bytes) - // 0x84+ : bytes data (variable length) - - // OUTPUT - upgradeToAndCall(address implementation, bytes data): - // 0x00-0x03: function selector (0x4f1ef286) (4 bytes) - // 0x04-0x23: implementation address (32 bytes) - // 0x24-0x43: offset to bytes data (0x40) (32 bytes) - // 0x44-0x63: length of bytes data (32 bytes) - // 0x64+ : bytes data (variable length) - - // Step 1: Extract proxy address from calldata - let proxy := shr(0x60, shl(0x60, calldataload(0x04))) - - // Step 2: Replace function selector - mstore(0x00, 0x4f1ef286) // upgradeToAndCall(address,bytes) - - // Step 3: Copy implementation address + data offset + data length + data - calldatacopy(0x20, 0x24, sub(calldatasize(), 0x24)) - - // Step 4: Fix data offset from 0x60 to 0x40 due to parameter removal - mstore(0x40, 0x40) - - // Execute call to proxy with transformed calldata - if iszero(call(gas(), proxy, callvalue(), 0x1c, sub(calldatasize(), 0x20), codesize(), 0x00)) { - let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) - } - } - // transferOwnership(address) - case 0xf2fde38b { - // Extract new owner address from calldata - let newOwner := shr(0x60, shl(0x60, calldataload(0x04))) - - // Verify new owner is not zero address - if iszero(shl(0x60, newOwner)) { - mstore(0x00, 0x54a56786) // InvalidNewOwner() - revert(0x1c, 0x04) - } - - // Emit {OwnershipTransferred} event - log3(codesize(), 0x00, OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(OWNER_SLOT), newOwner) - - // Store new owner in designated slot - sstore(OWNER_SLOT, newOwner) - } - // Unknown function selector - default { - mstore(0x00, 0x7352d91c) // InvalidSelector() - revert(0x1c, 0x04) - } - } - } + /// @notice Thrown when calldata length is insufficient for function call + error InvalidCalldataLength(); + + /// @notice Thrown when attempting to set invalid new owner + error InvalidNewOwner(); + + /// @notice Thrown when an unknown function selector is called + error InvalidSelector(); + + /// @notice Thrown when unauthorized account attempts admin operation + error UnauthorizedAccount(address account); + + /// @notice Emitted when ownership is transferred from one account to another + /// @param previousOwner Address of previous owner + /// @param newOwner Address of new owner + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Precomputed keccak256 hash for {OwnershipTransferred} event signature + /// @dev keccak256(bytes("OwnershipTransferred(address,address)")) + uint256 private constant OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE = + 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0; + + /// @notice Precomputed storage slot for owner using ERC-1967 standard + /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxyAdmin.owner"))) - 1) + uint256 private constant OWNER_SLOT = 0x9bc353c4ee8d049c7cb68b79467fc95d9015a8a82334bd0e61ce699e20cb5bd5; + + /// @notice Initializes the contract setting provided address as initial owner + /// @param initialOwner Address designated as initial owner of this contract + constructor(address initialOwner) { + assembly ("memory-safe") { + // Verify initial owner is not zero address + if iszero(shl(0x60, initialOwner)) { + mstore(0x00, 0x54a56786) // InvalidNewOwner() + revert(0x1c, 0x04) + } + + // Store initial owner in designated slot + sstore(OWNER_SLOT, initialOwner) + + // Emit {OwnershipTransferred} event + log3(codesize(), 0x00, OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0x00, initialOwner) + } + } + + fallback() external payable { + assembly ("memory-safe") { + // Ensure minimum calldata length + if lt(calldatasize(), 0x04) { + mstore(0x00, 0xca0ad260) // InvalidCalldataLength() + revert(0x1c, 0x04) + } + + // Extract function selector from calldata + let selector := shr(0xe0, calldataload(0x00)) + + // Stage 1: Permission-free functions (accessible by anyone) + switch selector + // owner() + case 0x8da5cb5b { + mstore(0x00, sload(OWNER_SLOT)) + return(0x00, 0x20) + } + // UPGRADE_INTERFACE_VERSION() + case 0xad3cb1cc { + // Compatible with OpenZeppelin's ProxyAdmin interface version + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/101bbaf1a8e02f95392380586ba0fc5752c4204d/contracts/proxy/transparent/ProxyAdmin.sol#L22 + mstore(0x00, 0x20) // String offset + mstore(0x20, 0x05) // String length + mstore(0x40, hex"352e302e30") // "5.0.0" in hex + return(0x00, 0x60) + } + + // Verify caller is authorized for owner-only functions + if iszero(eq(sload(OWNER_SLOT), caller())) { + mstore(0x00, 0x32b2baa3) // UnauthorizedAccount(address) + mstore(0x20, caller()) + revert(0x1c, 0x24) + } + + // Stage 2: Owner-only functions (authorization verified above) + switch selector + // upgradeAndCall(address,address,bytes) + case 0x9623609d { + // Calldata Transformation Process: + + // INPUT - upgradeAndCall(address proxy, address implementation, bytes data): + // 0x00-0x03: function selector (0x9623609d) (4 bytes) + // 0x04-0x23: proxy address (32 bytes) + // 0x24-0x43: implementation address (32 bytes) + // 0x44-0x63: offset to bytes data (0x60) (32 bytes) + // 0x64-0x83: length of bytes data (32 bytes) + // 0x84+ : bytes data (variable length) + + // OUTPUT - upgradeToAndCall(address implementation, bytes data): + // 0x00-0x03: function selector (0x4f1ef286) (4 bytes) + // 0x04-0x23: implementation address (32 bytes) + // 0x24-0x43: offset to bytes data (0x40) (32 bytes) + // 0x44-0x63: length of bytes data (32 bytes) + // 0x64+ : bytes data (variable length) + + // Step 1: Extract proxy address from calldata + let proxy := shr(0x60, shl(0x60, calldataload(0x04))) + + // Step 2: Replace function selector + mstore(0x00, 0x4f1ef286) // upgradeToAndCall(address,bytes) + + // Step 3: Copy implementation address + data offset + data length + data + calldatacopy(0x20, 0x24, sub(calldatasize(), 0x24)) + + // Step 4: Fix data offset from 0x60 to 0x40 due to parameter removal + mstore(0x40, 0x40) + + // Execute call to proxy with transformed calldata + if iszero(call(gas(), proxy, callvalue(), 0x1c, sub(calldatasize(), 0x20), codesize(), 0x00)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + } + // transferOwnership(address) + case 0xf2fde38b { + // Extract new owner address from calldata + let newOwner := shr(0x60, shl(0x60, calldataload(0x04))) + + // Verify new owner is not zero address + if iszero(shl(0x60, newOwner)) { + mstore(0x00, 0x54a56786) // InvalidNewOwner() + revert(0x1c, 0x04) + } + + // Emit {OwnershipTransferred} event + log3(codesize(), 0x00, OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(OWNER_SLOT), newOwner) + + // Store new owner in designated slot + sstore(OWNER_SLOT, newOwner) + } + // Unknown function selector + default { + mstore(0x00, 0x7352d91c) // InvalidSelector() + revert(0x1c, 0x04) + } + } + } } diff --git a/test/ProxyForge.fuzz.t.sol b/test/ProxyForge.fuzz.t.sol index 7caabaa..1a5502b 100644 --- a/test/ProxyForge.fuzz.t.sol +++ b/test/ProxyForge.fuzz.t.sol @@ -8,240 +8,237 @@ import {MockImplementationV1, MockImplementationV2} from "test/shared/mocks/Mock import {BaseTest} from "test/shared/BaseTest.sol"; contract ProxyForgeTestFuzzTest is BaseTest { - function setUp() public virtual override { - super.setUp(); - forge = new ProxyForge(); - } - - function test_fuzz_deploy(address caller, address owner) public assumeEOAs(caller, owner) impersonate(caller) { - address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - assertEq(proxy, forge.deploy(implementationV1, owner)); - verifyProxyStates(proxy, admin, implementationV1, owner); - } - - function test_fuzz_deployAndCall( - address caller, - address owner - ) public assumeEOAs(caller, owner) impersonate(caller) { - address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - assertEq(proxy, forge.deployAndCall(implementationV1, owner, "")); - verifyProxyStates(proxy, admin, implementationV1, owner); - } - - function test_fuzz_deployAndCall( - address caller, - address owner, - uint256 initValue, - uint256 value - ) public assumeEOAs(caller, owner) impersonate(caller) { - if (value != uint256(0)) vm.deal(caller, value); - - address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - assertEq(proxy, forge.deployAndCall{value: value}(implementationV1, owner, encodeV1Data(initValue))); - assertEq(proxy.balance, value); - verifyProxyStates(proxy, admin, implementationV1, owner); - } - - function test_fuzz_deployDeterministic( - bool protected, - address caller, - address owner, - uint96 identifier - ) public assumeEOAs(caller, owner) impersonate(caller) { - bytes32 salt = encodeSalt(protected ? caller : address(0), identifier); - address predicted = forge.computeProxyAddress(implementationV1, salt, ""); - address admin = forge.adminOf(predicted); - - try forge.deployDeterministic(implementationV1, owner, salt) returns (address proxy) { - assertEq(proxy, predicted); - verifyProxyStates(proxy, admin, implementationV1, owner); - } catch { - // Deployment might fail due to address collision, which is acceptable - } - } - - function test_fuzz_deployDeterministic_revertsWithInvalidSalt( - address caller, - address other, - uint96 identifier - ) public assumeEOAs(caller, other) impersonate(caller) { - bytes32 salt = encodeSalt(other, identifier); - vm.expectRevert(IProxyForge.InvalidSalt.selector); - forge.deployDeterministic(implementationV1, caller, salt); - } - - function test_fuzz_deployDeterministicAndCall( - bool protected, - address caller, - address owner, - uint96 identifier - ) public assumeEOAs(caller, owner) impersonate(caller) { - bytes32 salt = encodeSalt(protected ? caller : address(0), identifier); - address predicted = forge.computeProxyAddress(implementationV1, salt, ""); - address admin = forge.adminOf(predicted); - - try forge.deployDeterministicAndCall(implementationV1, owner, salt, "") returns (address proxy) { - assertEq(proxy, predicted); - verifyProxyStates(proxy, admin, implementationV1, owner); - } catch { - // Deployment might fail due to address collision, which is acceptable - } - } - - function test_fuzz_deployDeterministicAndCall( - bool protected, - address caller, - address owner, - uint96 identifier, - uint256 initValue, - uint256 value - ) public assumeEOAs(caller, owner) impersonate(caller) { - if (value != uint256(0)) vm.deal(caller, value); - - bytes32 salt = encodeSalt(protected ? caller : address(0), identifier); - bytes memory data = encodeV1Data(initValue); - - address predicted = forge.computeProxyAddress(implementationV1, salt, data); - address admin = forge.adminOf(predicted); - - try forge.deployDeterministicAndCall{value: value}(implementationV1, owner, salt, data) returns ( - address proxy - ) { - assertEq(proxy, predicted); - assertEq(proxy.balance, value); - verifyProxyStates(proxy, admin, implementationV1, owner); - } catch { - // Deployment might fail due to address collision, which is acceptable - } - } - - function test_fuzz_upgrade(address caller) public assumeEOA(caller) impersonate(caller) { - address proxy = forge.deployAndCall(implementationV1, caller, encodeV1Data(100)); - - forge.upgrade(proxy, implementationV2); - assertEq(forge.implementationOf(proxy), implementationV2); - - forge.upgrade(proxy, implementationV1); - assertEq(forge.implementationOf(proxy), implementationV1); - } - - function test_fuzz_upgrade_revertWithUnauthorizedAccount( - address owner, - address invalidOwner - ) public assumeEOAs(owner, invalidOwner) { - address proxy = forge.deployAndCall(implementationV1, owner, encodeV1Data(100)); - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, invalidOwner)); - vm.prank(invalidOwner); - forge.upgrade(proxy, implementationV2); - } - - function test_fuzz_upgradeAndCall(address caller) public assumeEOA(caller) impersonate(caller) { - address proxy = forge.deployAndCall(implementationV1, caller, encodeV1Data(100)); - assertEq(forge.implementationOf(proxy), implementationV1); - - forge.upgradeAndCall(proxy, implementationV2, ""); - assertEq(forge.implementationOf(proxy), implementationV2); - } - - function test_fuzz_upgradeAndCall( - address caller, - string memory data, - uint256 value - ) public assumeEOA(caller) impersonate(caller) { - if (value != uint256(0)) vm.deal(caller, value); - - address proxy = forge.deployAndCall(implementationV1, caller, encodeV1Data(100)); - assertEq(forge.implementationOf(proxy), implementationV1); - - forge.upgradeAndCall{value: value}(proxy, implementationV2, encodeV2Data(data)); - assertEq(forge.implementationOf(proxy), implementationV2); - assertEq(proxy.balance, value); - } - - function test_fuzz_revoke(address caller) public assumeEOA(caller) impersonate(caller) { - address proxy = forge.deploy(implementationV1, caller); - address admin = forge.adminOf(proxy); - assertEq(getProxyAdminOwner(admin), address(forge)); - assertEq(forge.implementationOf(proxy), implementationV1); - assertEq(forge.ownerOf(proxy), caller); - - vm.expectEmit(true, true, true, true, address(forge)); - emit IProxyForge.ProxyRevoked(proxy); - - forge.revoke(proxy); - assertEq(getProxyAdminOwner(admin), caller); - assertEq(forge.implementationOf(proxy), address(0)); - assertEq(forge.ownerOf(proxy), address(0)); - } - - function test_fuzz_changeOwner(address oldOwner, address newOwner) public assumeEOAs(oldOwner, newOwner) { - address proxy = forge.deploy(implementationV1, oldOwner); - assertEq(forge.implementationOf(proxy), implementationV1); - assertEq(forge.ownerOf(proxy), oldOwner); - - vm.prank(oldOwner); - forge.changeOwner(proxy, newOwner); - assertEq(forge.ownerOf(proxy), newOwner); - - vm.prank(newOwner); - forge.upgrade(proxy, implementationV2); - assertEq(forge.implementationOf(proxy), implementationV2); - - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, oldOwner)); - vm.prank(oldOwner); - forge.upgrade(proxy, implementationV1); - } - - function test_fuzz_computeProxyAddress(uint256 nonce) public { - if (nonce >= type(uint64).max) { - vm.expectRevert(CreateX.InvalidNonce.selector); - forge.computeProxyAddress(nonce); - } else { - address computed = forge.computeProxyAddress(nonce); - address expected = vm.computeCreateAddress(address(forge), nonce); - assertEq(computed, expected); - } - } - - function test_fuzz_computeProxyAddress(bytes32 salt1, bytes32 salt2, uint256 initValue) public view { - vm.assume(salt1 != salt2); - bytes memory data = encodeV1Data(initValue); - address computed1 = forge.computeProxyAddress(implementationV1, salt1, data); - address computed2 = forge.computeProxyAddress(implementationV1, salt2, data); - assertNotEq(computed1, computed2); - } - - function test_fuzz_deploy_uniqueness(uint8 numDeployments) public { - numDeployments = uint8(bound(numDeployments, 1, 20)); - address[] memory proxies = new address[](numDeployments); - for (uint256 i = 0; i < numDeployments; ++i) { - proxies[i] = forge.deploy(implementationV1, address(this)); - } - assertFalse(hasDuplicate(proxies)); - } - - function hasDuplicate(address[] memory array) internal pure returns (bool result) { - assembly ("memory-safe") { - function p(i, x) -> y { - y := or(shr(i, x), x) - } - let n := mload(array) - // prettier-ignore - if iszero(lt(n, 2)) { - let m := mload(0x40) - let w := not(0x1f) - let c := and(w, p(16, p(8, p(4, p(2, p(1, mul(0x30, n))))))) - calldatacopy(m, calldatasize(), add(0x20, c)) - for { let i := add(array, shl(5, n)) } 1 {} { + function setUp() public virtual override { + super.setUp(); + forge = new ProxyForge(); + } + + function test_fuzz_deploy(address caller, address owner) public assumeEOAs(caller, owner) impersonate(caller) { + address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + assertEq(proxy, forge.deploy(implementationV1, owner)); + verifyProxyStates(proxy, admin, implementationV1, owner); + } + + function test_fuzz_deployAndCall(address caller, address owner) + public + assumeEOAs(caller, owner) + impersonate(caller) + { + address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + assertEq(proxy, forge.deployAndCall(implementationV1, owner, "")); + verifyProxyStates(proxy, admin, implementationV1, owner); + } + + function test_fuzz_deployAndCall(address caller, address owner, uint256 initValue, uint256 value) + public + assumeEOAs(caller, owner) + impersonate(caller) + { + if (value != uint256(0)) vm.deal(caller, value); + + address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + assertEq(proxy, forge.deployAndCall{value: value}(implementationV1, owner, encodeV1Data(initValue))); + assertEq(proxy.balance, value); + verifyProxyStates(proxy, admin, implementationV1, owner); + } + + function test_fuzz_deployDeterministic(bool protected, address caller, address owner, uint96 identifier) + public + assumeEOAs(caller, owner) + impersonate(caller) + { + bytes32 salt = encodeSalt(protected ? caller : address(0), identifier); + address predicted = forge.computeProxyAddress(implementationV1, salt, ""); + address admin = forge.adminOf(predicted); + + try forge.deployDeterministic(implementationV1, owner, salt) returns (address proxy) { + assertEq(proxy, predicted); + verifyProxyStates(proxy, admin, implementationV1, owner); + } catch { + // Deployment might fail due to address collision, which is acceptable + } + } + + function test_fuzz_deployDeterministic_revertsWithInvalidSalt(address caller, address other, uint96 identifier) + public + assumeEOAs(caller, other) + impersonate(caller) + { + bytes32 salt = encodeSalt(other, identifier); + vm.expectRevert(IProxyForge.InvalidSalt.selector); + forge.deployDeterministic(implementationV1, caller, salt); + } + + function test_fuzz_deployDeterministicAndCall(bool protected, address caller, address owner, uint96 identifier) + public + assumeEOAs(caller, owner) + impersonate(caller) + { + bytes32 salt = encodeSalt(protected ? caller : address(0), identifier); + address predicted = forge.computeProxyAddress(implementationV1, salt, ""); + address admin = forge.adminOf(predicted); + + try forge.deployDeterministicAndCall(implementationV1, owner, salt, "") returns (address proxy) { + assertEq(proxy, predicted); + verifyProxyStates(proxy, admin, implementationV1, owner); + } catch { + // Deployment might fail due to address collision, which is acceptable + } + } + + function test_fuzz_deployDeterministicAndCall( + bool protected, + address caller, + address owner, + uint96 identifier, + uint256 initValue, + uint256 value + ) public assumeEOAs(caller, owner) impersonate(caller) { + if (value != uint256(0)) vm.deal(caller, value); + + bytes32 salt = encodeSalt(protected ? caller : address(0), identifier); + bytes memory data = encodeV1Data(initValue); + + address predicted = forge.computeProxyAddress(implementationV1, salt, data); + address admin = forge.adminOf(predicted); + + try forge.deployDeterministicAndCall{value: value}(implementationV1, owner, salt, data) returns (address proxy) + { + assertEq(proxy, predicted); + assertEq(proxy.balance, value); + verifyProxyStates(proxy, admin, implementationV1, owner); + } catch { + // Deployment might fail due to address collision, which is acceptable + } + } + + function test_fuzz_upgrade(address caller) public assumeEOA(caller) impersonate(caller) { + address proxy = forge.deployAndCall(implementationV1, caller, encodeV1Data(100)); + + forge.upgrade(proxy, implementationV2); + assertEq(forge.implementationOf(proxy), implementationV2); + + forge.upgrade(proxy, implementationV1); + assertEq(forge.implementationOf(proxy), implementationV1); + } + + function test_fuzz_upgrade_revertWithUnauthorizedAccount(address owner, address invalidOwner) + public + assumeEOAs(owner, invalidOwner) + { + address proxy = forge.deployAndCall(implementationV1, owner, encodeV1Data(100)); + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, invalidOwner)); + vm.prank(invalidOwner); + forge.upgrade(proxy, implementationV2); + } + + function test_fuzz_upgradeAndCall(address caller) public assumeEOA(caller) impersonate(caller) { + address proxy = forge.deployAndCall(implementationV1, caller, encodeV1Data(100)); + assertEq(forge.implementationOf(proxy), implementationV1); + + forge.upgradeAndCall(proxy, implementationV2, ""); + assertEq(forge.implementationOf(proxy), implementationV2); + } + + function test_fuzz_upgradeAndCall(address caller, string memory data, uint256 value) + public + assumeEOA(caller) + impersonate(caller) + { + if (value != uint256(0)) vm.deal(caller, value); + + address proxy = forge.deployAndCall(implementationV1, caller, encodeV1Data(100)); + assertEq(forge.implementationOf(proxy), implementationV1); + + forge.upgradeAndCall{value: value}(proxy, implementationV2, encodeV2Data(data)); + assertEq(forge.implementationOf(proxy), implementationV2); + assertEq(proxy.balance, value); + } + + function test_fuzz_revoke(address caller) public assumeEOA(caller) impersonate(caller) { + address proxy = forge.deploy(implementationV1, caller); + address admin = forge.adminOf(proxy); + assertEq(getProxyAdminOwner(admin), address(forge)); + assertEq(forge.implementationOf(proxy), implementationV1); + assertEq(forge.ownerOf(proxy), caller); + + vm.expectEmit(true, true, true, true, address(forge)); + emit IProxyForge.ProxyRevoked(proxy); + + forge.revoke(proxy); + assertEq(getProxyAdminOwner(admin), caller); + assertEq(forge.implementationOf(proxy), address(0)); + assertEq(forge.ownerOf(proxy), address(0)); + } + + function test_fuzz_changeOwner(address oldOwner, address newOwner) public assumeEOAs(oldOwner, newOwner) { + address proxy = forge.deploy(implementationV1, oldOwner); + assertEq(forge.implementationOf(proxy), implementationV1); + assertEq(forge.ownerOf(proxy), oldOwner); + + vm.prank(oldOwner); + forge.changeOwner(proxy, newOwner); + assertEq(forge.ownerOf(proxy), newOwner); + + vm.prank(newOwner); + forge.upgrade(proxy, implementationV2); + assertEq(forge.implementationOf(proxy), implementationV2); + + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, oldOwner)); + vm.prank(oldOwner); + forge.upgrade(proxy, implementationV1); + } + + function test_fuzz_computeProxyAddress(uint256 nonce) public { + if (nonce >= type(uint64).max) { + vm.expectRevert(CreateX.InvalidNonce.selector); + forge.computeProxyAddress(nonce); + } else { + address computed = forge.computeProxyAddress(nonce); + address expected = vm.computeCreateAddress(address(forge), nonce); + assertEq(computed, expected); + } + } + + function test_fuzz_computeProxyAddress(bytes32 salt1, bytes32 salt2, uint256 initValue) public view { + vm.assume(salt1 != salt2); + bytes memory data = encodeV1Data(initValue); + address computed1 = forge.computeProxyAddress(implementationV1, salt1, data); + address computed2 = forge.computeProxyAddress(implementationV1, salt2, data); + assertNotEq(computed1, computed2); + } + + function test_fuzz_deploy_uniqueness(uint8 numDeployments) public { + numDeployments = uint8(bound(numDeployments, 1, 20)); + address[] memory proxies = new address[](numDeployments); + for (uint256 i = 0; i < numDeployments; ++i) { + proxies[i] = forge.deploy(implementationV1, address(this)); + } + assertFalse(hasDuplicate(proxies)); + } + + function hasDuplicate(address[] memory array) internal pure returns (bool result) { + assembly ("memory-safe") { + function p(i, x) -> y { + y := or(shr(i, x), x) + } + let n := mload(array) + // prettier-ignore + if iszero(lt(n, 2)) { + let m := mload(0x40) + let w := not(0x1f) + let c := and(w, p(16, p(8, p(4, p(2, p(1, mul(0x30, n))))))) + calldatacopy(m, calldatasize(), add(0x20, c)) + for { let i := add(array, shl(5, n)) } 1 {} { let r := mulmod(mload(i), 0x100000000000000000000000000000051, not(0xbc)) for {} 1 { r := add(0x20, r) } { let o := add(m, and(r, c)) @@ -258,8 +255,8 @@ contract ProxyForgeTestFuzzTest is BaseTest { i := add(i, w) if iszero(lt(array, i)) { break } } - if shr(31, n) { invalid() } - } - } - } + if shr(31, n) { invalid() } + } + } + } } diff --git a/test/ProxyForge.t.sol b/test/ProxyForge.t.sol index 218f414..1b537fb 100644 --- a/test/ProxyForge.t.sol +++ b/test/ProxyForge.t.sol @@ -9,358 +9,362 @@ import {MockImplementationV1, MockImplementationV2} from "test/shared/mocks/Mock import {BaseTest} from "test/shared/BaseTest.sol"; contract ProxyForgeTest is BaseTest { - function setUp() public virtual override { - super.setUp(); - forge = new ProxyForge(); - } - - function test_deploy() public { - address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - expectEmitDeployEvents(proxy, admin, implementationV1, alice, bytes32(0)); - assertEq(proxy, forge.deploy(implementationV1, alice)); - verifyProxyStates(proxy, admin, implementationV1, alice); - } - - function test_deploy_revertsWithContractCreationFailed_invalidImplementation() public { - vm.expectRevert(CreateX.ContractCreationFailed.selector); - forge.deploy(address(0), alice); - - vm.expectRevert(CreateX.ContractCreationFailed.selector); - forge.deploy(address(0xdeadbeef), alice); - } - - function test_deploy_revertsWithInvalidProxyOwner() public { - vm.expectRevert(IProxyForge.InvalidProxyOwner.selector); - forge.deploy(implementationV1, address(0)); - } - - function test_deployAndCall() public { - address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - expectEmitDeployEvents(proxy, admin, implementationV1, alice, bytes32(0)); - assertEq(proxy, forge.deployAndCall(implementationV1, alice, encodeV1Data(100))); - verifyProxyStates(proxy, admin, implementationV1, alice); - - MockImplementationV1 mockProxy = MockImplementationV1(proxy); - assertEq(mockProxy.version(), "1"); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.getValue(), 100); - } - - function test_deployAndCall_withCallValue() public { - address proxy = forge.deployAndCall{value: 5 ether}(implementationV1, alice, encodeV1Data(100)); - assertEq(proxy.balance, 5 ether); - } - - function test_deployAndCall_withLargeData() public { - bytes memory data = new bytes(1000); - for (uint256 i = 0; i < 1000; i++) data[i] = bytes1(uint8(i % 256)); - - address proxy = forge.deployAndCall(implementationV1, alice, encodeV1Data(abi.decode(data, (uint256)))); - assertTrue(isContract(proxy)); - } - - function test_deployAndCall_multipleDeployments() public { - for (uint256 i = 0; i < 10; ++i) { - address proxy = forge.deployAndCall(implementationV1, alice, encodeV1Data(100 + i)); - assertEq(forge.adminOf(proxy), getProxyAdmin(proxy)); - assertEq(forge.implementationOf(proxy), implementationV1); - assertEq(forge.ownerOf(proxy), alice); - assertEq(MockImplementationV1(proxy).getValue(), 100 + i); - } - } - - function test_deployDeterministic() public { - bytes32 salt = generateSalt(address(this)); - address proxy = forge.computeProxyAddress(implementationV1, salt, ""); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - assertEq(proxy, forge.deployDeterministic(implementationV1, alice, salt)); - verifyProxyStates(proxy, admin, implementationV1, alice); - } - - function test_deployDeterministic_zeroSalt() public { - address proxy = forge.deployDeterministic(implementationV1, alice, bytes32(0)); - assertTrue(isContract(proxy)); - } - - function test_deployDeterministic_revertsWithInvalidSalt() public { - bytes32 salt = generateSalt(bob); - vm.expectRevert(IProxyForge.InvalidSalt.selector); - vm.prank(alice); - forge.deployDeterministic(implementationV1, alice, salt); - } - - function test_deployDeterministic_revertsWithContractCreationFailed_addressCollision() public { - bytes32 salt = generateSalt(address(this)); - address proxy = forge.deployDeterministic(implementationV1, alice, salt); - assertTrue(isContract(proxy)); - - vm.expectRevert(CreateX.ContractCreationFailed.selector); - forge.deployDeterministic(implementationV1, alice, salt); - } - - function test_deployDeterministicAndCall() public { - bytes32 salt = generateSalt(address(this)); - bytes memory data = encodeV1Data(200); - - address proxy = forge.computeProxyAddress(implementationV1, salt, data); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - assertEq(proxy, forge.deployDeterministicAndCall(implementationV1, alice, salt, data)); - verifyProxyStates(proxy, admin, implementationV1, alice); - - MockImplementationV1 mockProxy = MockImplementationV1(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "1"); - assertEq(mockProxy.getValue(), 200); - } - - function test_deployDeterministicAndCall_withCallValue() public { - bytes32 salt = generateSalt(address(this)); - bytes memory data = encodeV1Data(200); - - address proxy = forge.computeProxyAddress(implementationV1, salt, data); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - assertEq(proxy, forge.deployDeterministicAndCall{value: 5 ether}(implementationV1, alice, salt, data)); - verifyProxyStates(proxy, admin, implementationV1, alice); - assertEq(proxy.balance, 5 ether); - } - - function test_upgradeAndCall_withLargeData() public { - bytes memory data = new bytes(1000); - for (uint256 i = 0; i < 1000; i++) data[i] = bytes1(uint8(i % 256)); - - address proxy = forge.deployAndCall(implementationV1, address(this), encodeV1Data(abi.decode(data, (uint256)))); - forge.upgradeAndCall(proxy, implementationV2, encodeV2Data(string(data))); - assertTrue(isContract(proxy)); - } - - function test_deployDeterministicAndCall_multipleDeployments() public { - for (uint256 i = 0; i < 10; ++i) { - bytes32 salt = encodeSalt(address(this), uint96(i)); - address proxy = forge.deployDeterministicAndCall(implementationV1, alice, salt, encodeV1Data(100 + i)); - assertEq(forge.adminOf(proxy), getProxyAdmin(proxy)); - assertEq(forge.implementationOf(proxy), implementationV1); - assertEq(forge.ownerOf(proxy), alice); - assertEq(MockImplementationV1(proxy).getValue(), 100 + i); - } - } - - function test_upgrade() public { - address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); - address admin = forge.adminOf(proxy); - assertTrue(!isContract(proxy) && !isContract(admin)); - - expectEmitDeployEvents(proxy, admin, implementationV1, alice, bytes32(0)); - assertEq(proxy, forge.deployAndCall(implementationV1, alice, encodeV1Data(100))); - verifyProxyStates(proxy, admin, implementationV1, alice); - - expectEmitUpgradeEvents(proxy, implementationV2); - vm.prank(alice); - forge.upgrade(proxy, implementationV2); - - assertEq(forge.implementationOf(proxy), implementationV2); - assertEq(MockImplementationV2(proxy).version(), "2"); - } - - function test_upgrade_revertsWithUnauthorizedAccount() public { - address proxy = forge.deploy(implementationV1, alice); - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); - forge.upgrade(proxy, implementationV2); - } - - function test_upgrade_revertsWithInvalidProxyImplementation() public { - address proxy = forge.deploy(implementationV1, address(this)); - vm.expectRevert(IProxyForge.InvalidProxyImplementation.selector); - forge.upgrade(proxy, implementationV1); - } - - function test_upgrade_revertsWithInvalidImplementation() public { - address proxy = forge.deploy(implementationV1, address(this)); - - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - forge.upgrade(proxy, address(0)); - - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - forge.upgrade(proxy, address(0xdeadbeef)); - } - - function test_upgradeAndCall() public { - address proxy = forge.deployAndCall(implementationV1, address(this), encodeV1Data(100)); - - expectEmitUpgradeEvents(proxy, implementationV2); - forge.upgradeAndCall(proxy, implementationV2, encodeV2Data("ProxyForge")); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getData(), "ProxyForge"); - assertEq(mockProxy.getValue(), 100); - } - - function test_upgradeAndCall_withCallValue() public { - address proxy = forge.deployAndCall(implementationV1, address(this), encodeV1Data(100)); - - expectEmitUpgradeEvents(proxy, implementationV2); - forge.upgradeAndCall{value: 5 ether}(proxy, implementationV2, encodeV2Data("ProxyForge")); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getData(), "ProxyForge"); - assertEq(mockProxy.getValue(), 100); - assertEq(proxy.balance, 5 ether); - } - - function test_upgradeAndCall_multipleProxies() public { - address[] memory proxies = new address[](10); - - for (uint256 i = 0; i < proxies.length; ++i) { - address proxy = proxies[i] = forge.deployAndCall(implementationV1, address(this), encodeV1Data(100 + i)); - assertEq(forge.implementationOf(proxy), implementationV1); - assertEq(MockImplementationV1(proxy).getValue(), 100 + i); - } - - for (uint256 i = 0; i < proxies.length; ++i) { - string memory data = vm.toString(i); - forge.upgradeAndCall(proxies[i], implementationV2, encodeV2Data(data)); - assertEq(forge.implementationOf(proxies[i]), implementationV2); - assertEq(MockImplementationV2(proxies[i]).getData(), data); - } - } - - function test_revoke() public { - address proxy = forge.deploy(implementationV1, alice); - address admin = forge.adminOf(proxy); - assertEq(getProxyAdminOwner(admin), address(forge)); - assertEq(forge.implementationOf(proxy), implementationV1); - assertEq(forge.ownerOf(proxy), alice); - - vm.expectEmit(true, true, true, true, address(forge)); - emit IProxyForge.ProxyRevoked(proxy); - - vm.prank(alice); - forge.revoke(proxy); - assertEq(getProxyAdminOwner(admin), alice); - assertEq(forge.implementationOf(proxy), address(0)); - assertEq(forge.ownerOf(proxy), address(0)); - } - - function test_revoke_revertsWithUnauthorizedAccount() public { - address proxy = forge.deploy(implementationV1, alice); - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); - forge.revoke(proxy); - } - - function test_changeOwner() public { - address proxy = forge.deploy(implementationV1, alice); - - vm.expectEmit(true, true, true, true, address(forge)); - emit IProxyForge.ProxyOwnerChanged(proxy, bob); - - vm.startPrank(alice); - forge.changeOwner(proxy, bob); - assertEq(forge.ownerOf(proxy), bob); - - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, alice)); - forge.upgrade(proxy, implementationV2); - vm.stopPrank(); - - expectEmitUpgradeEvents(proxy, implementationV2); - vm.prank(bob); - forge.upgrade(proxy, implementationV2); - } - - function test_changeOwner_revertsWithUnauthorizedAccount() public { - address proxy = forge.deploy(implementationV1, alice); - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, bob)); - vm.prank(bob); - forge.changeOwner(proxy, bob); - } - - function test_changeOwner_revertsWithInvalidProxyOwner() public { - address proxy = forge.deploy(implementationV1, address(this)); - vm.expectRevert(IProxyForge.InvalidProxyOwner.selector); - forge.changeOwner(proxy, address(0)); - } - - function test_adminOf() public { - address proxy = forge.deploy(implementationV1, alice); - address admin = forge.adminOf(proxy); - assertEq(admin, getProxyAdmin(proxy)); - assertEq(admin, vm.computeCreateAddress(proxy, uint256(1))); - } - - function test_implementationOf() public { - address proxy = forge.deploy(implementationV1, alice); - assertEq(getProxyImplementation(proxy), implementationV1); - assertEq(forge.implementationOf(proxy), implementationV1); - } - - function test_ownerOf() public { - address proxy = forge.deploy(implementationV1, alice); - assertEq(forge.ownerOf(proxy), alice); - } - - function test_computeProxyAddress_CREATE() public { - address predicted = forge.computeProxyAddress(vm.getNonce(address(forge))); - address deployed = forge.deploy(implementationV1, alice); - assertEq(deployed, predicted); - } - - function test_computeProxyAddress_CREATE2() public { - bytes32 salt = generateSalt(address(this)); - bytes memory data = encodeV1Data(100); - - address predicted = forge.computeProxyAddress(implementationV1, salt, data); - address deployed = forge.deployDeterministicAndCall(implementationV1, alice, salt, data); - assertEq(deployed, predicted); - } - - function test_integration_completeLifecycle() public { - address proxy = forge.deployAndCall(implementationV1, alice, encodeV1Data(100)); - address admin = forge.adminOf(proxy); - - MockImplementationV1 mockProxyV1 = MockImplementationV1(proxy); - assertEq(mockProxyV1.version(), "1"); - assertEq(mockProxyV1.getValue(), 100); - assertEq(mockProxyV1.setValue(200), 200); - - vm.expectRevert(ForgeProxy.ProxyDeniedAdminAccess.selector); - vm.prank(admin); - mockProxyV1.getValue(); - - bytes memory data = encodeV2Data("ProxyForge"); - - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); - forge.upgradeAndCall(proxy, implementationV2, data); - - vm.prank(alice); - forge.upgradeAndCall(proxy, implementationV2, data); - assertEq(forge.implementationOf(proxy), implementationV2); - - MockImplementationV2 mockProxyV2 = MockImplementationV2(proxy); - assertEq(mockProxyV2.version(), "2"); - assertEq(mockProxyV2.getValue(), 200); - assertEq(mockProxyV2.getData(), "ProxyForge"); - - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); - forge.changeOwner(proxy, bob); - - vm.prank(alice); - forge.changeOwner(proxy, bob); - assertEq(forge.ownerOf(proxy), bob); - - vm.prank(bob); - forge.upgrade(proxy, implementationV1); - assertEq(forge.implementationOf(proxy), implementationV1); - } + function setUp() public virtual override { + super.setUp(); + forge = new ProxyForge(); + } + + function test_deploy() public { + address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + expectEmitDeployEvents(proxy, admin, implementationV1, alice, bytes32(0)); + assertEq(proxy, forge.deploy(implementationV1, alice)); + verifyProxyStates(proxy, admin, implementationV1, alice); + } + + function test_deploy_revertsWithContractCreationFailed_invalidImplementation() public { + vm.expectRevert(CreateX.ContractCreationFailed.selector); + forge.deploy(address(0), alice); + + vm.expectRevert(CreateX.ContractCreationFailed.selector); + forge.deploy(address(0xdeadbeef), alice); + } + + function test_deploy_revertsWithInvalidProxyOwner() public { + vm.expectRevert(IProxyForge.InvalidProxyOwner.selector); + forge.deploy(implementationV1, address(0)); + } + + function test_deployAndCall() public { + address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + expectEmitDeployEvents(proxy, admin, implementationV1, alice, bytes32(0)); + assertEq(proxy, forge.deployAndCall(implementationV1, alice, encodeV1Data(100))); + verifyProxyStates(proxy, admin, implementationV1, alice); + + MockImplementationV1 mockProxy = MockImplementationV1(proxy); + assertEq(mockProxy.version(), "1"); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.getValue(), 100); + } + + function test_deployAndCall_withCallValue() public { + address proxy = forge.deployAndCall{value: 5 ether}(implementationV1, alice, encodeV1Data(100)); + assertEq(proxy.balance, 5 ether); + } + + function test_deployAndCall_withLargeData() public { + bytes memory data = new bytes(1000); + for (uint256 i = 0; i < 1000; i++) { + data[i] = bytes1(uint8(i % 256)); + } + + address proxy = forge.deployAndCall(implementationV1, alice, encodeV1Data(abi.decode(data, (uint256)))); + assertTrue(isContract(proxy)); + } + + function test_deployAndCall_multipleDeployments() public { + for (uint256 i = 0; i < 10; ++i) { + address proxy = forge.deployAndCall(implementationV1, alice, encodeV1Data(100 + i)); + assertEq(forge.adminOf(proxy), getProxyAdmin(proxy)); + assertEq(forge.implementationOf(proxy), implementationV1); + assertEq(forge.ownerOf(proxy), alice); + assertEq(MockImplementationV1(proxy).getValue(), 100 + i); + } + } + + function test_deployDeterministic() public { + bytes32 salt = generateSalt(address(this)); + address proxy = forge.computeProxyAddress(implementationV1, salt, ""); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + assertEq(proxy, forge.deployDeterministic(implementationV1, alice, salt)); + verifyProxyStates(proxy, admin, implementationV1, alice); + } + + function test_deployDeterministic_zeroSalt() public { + address proxy = forge.deployDeterministic(implementationV1, alice, bytes32(0)); + assertTrue(isContract(proxy)); + } + + function test_deployDeterministic_revertsWithInvalidSalt() public { + bytes32 salt = generateSalt(bob); + vm.expectRevert(IProxyForge.InvalidSalt.selector); + vm.prank(alice); + forge.deployDeterministic(implementationV1, alice, salt); + } + + function test_deployDeterministic_revertsWithContractCreationFailed_addressCollision() public { + bytes32 salt = generateSalt(address(this)); + address proxy = forge.deployDeterministic(implementationV1, alice, salt); + assertTrue(isContract(proxy)); + + vm.expectRevert(CreateX.ContractCreationFailed.selector); + forge.deployDeterministic(implementationV1, alice, salt); + } + + function test_deployDeterministicAndCall() public { + bytes32 salt = generateSalt(address(this)); + bytes memory data = encodeV1Data(200); + + address proxy = forge.computeProxyAddress(implementationV1, salt, data); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + assertEq(proxy, forge.deployDeterministicAndCall(implementationV1, alice, salt, data)); + verifyProxyStates(proxy, admin, implementationV1, alice); + + MockImplementationV1 mockProxy = MockImplementationV1(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "1"); + assertEq(mockProxy.getValue(), 200); + } + + function test_deployDeterministicAndCall_withCallValue() public { + bytes32 salt = generateSalt(address(this)); + bytes memory data = encodeV1Data(200); + + address proxy = forge.computeProxyAddress(implementationV1, salt, data); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + assertEq(proxy, forge.deployDeterministicAndCall{value: 5 ether}(implementationV1, alice, salt, data)); + verifyProxyStates(proxy, admin, implementationV1, alice); + assertEq(proxy.balance, 5 ether); + } + + function test_upgradeAndCall_withLargeData() public { + bytes memory data = new bytes(1000); + for (uint256 i = 0; i < 1000; i++) { + data[i] = bytes1(uint8(i % 256)); + } + + address proxy = forge.deployAndCall(implementationV1, address(this), encodeV1Data(abi.decode(data, (uint256)))); + forge.upgradeAndCall(proxy, implementationV2, encodeV2Data(string(data))); + assertTrue(isContract(proxy)); + } + + function test_deployDeterministicAndCall_multipleDeployments() public { + for (uint256 i = 0; i < 10; ++i) { + bytes32 salt = encodeSalt(address(this), uint96(i)); + address proxy = forge.deployDeterministicAndCall(implementationV1, alice, salt, encodeV1Data(100 + i)); + assertEq(forge.adminOf(proxy), getProxyAdmin(proxy)); + assertEq(forge.implementationOf(proxy), implementationV1); + assertEq(forge.ownerOf(proxy), alice); + assertEq(MockImplementationV1(proxy).getValue(), 100 + i); + } + } + + function test_upgrade() public { + address proxy = forge.computeProxyAddress(vm.getNonce(address(forge))); + address admin = forge.adminOf(proxy); + assertTrue(!isContract(proxy) && !isContract(admin)); + + expectEmitDeployEvents(proxy, admin, implementationV1, alice, bytes32(0)); + assertEq(proxy, forge.deployAndCall(implementationV1, alice, encodeV1Data(100))); + verifyProxyStates(proxy, admin, implementationV1, alice); + + expectEmitUpgradeEvents(proxy, implementationV2); + vm.prank(alice); + forge.upgrade(proxy, implementationV2); + + assertEq(forge.implementationOf(proxy), implementationV2); + assertEq(MockImplementationV2(proxy).version(), "2"); + } + + function test_upgrade_revertsWithUnauthorizedAccount() public { + address proxy = forge.deploy(implementationV1, alice); + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); + forge.upgrade(proxy, implementationV2); + } + + function test_upgrade_revertsWithInvalidProxyImplementation() public { + address proxy = forge.deploy(implementationV1, address(this)); + vm.expectRevert(IProxyForge.InvalidProxyImplementation.selector); + forge.upgrade(proxy, implementationV1); + } + + function test_upgrade_revertsWithInvalidImplementation() public { + address proxy = forge.deploy(implementationV1, address(this)); + + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + forge.upgrade(proxy, address(0)); + + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + forge.upgrade(proxy, address(0xdeadbeef)); + } + + function test_upgradeAndCall() public { + address proxy = forge.deployAndCall(implementationV1, address(this), encodeV1Data(100)); + + expectEmitUpgradeEvents(proxy, implementationV2); + forge.upgradeAndCall(proxy, implementationV2, encodeV2Data("ProxyForge")); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getData(), "ProxyForge"); + assertEq(mockProxy.getValue(), 100); + } + + function test_upgradeAndCall_withCallValue() public { + address proxy = forge.deployAndCall(implementationV1, address(this), encodeV1Data(100)); + + expectEmitUpgradeEvents(proxy, implementationV2); + forge.upgradeAndCall{value: 5 ether}(proxy, implementationV2, encodeV2Data("ProxyForge")); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getData(), "ProxyForge"); + assertEq(mockProxy.getValue(), 100); + assertEq(proxy.balance, 5 ether); + } + + function test_upgradeAndCall_multipleProxies() public { + address[] memory proxies = new address[](10); + + for (uint256 i = 0; i < proxies.length; ++i) { + address proxy = proxies[i] = forge.deployAndCall(implementationV1, address(this), encodeV1Data(100 + i)); + assertEq(forge.implementationOf(proxy), implementationV1); + assertEq(MockImplementationV1(proxy).getValue(), 100 + i); + } + + for (uint256 i = 0; i < proxies.length; ++i) { + string memory data = vm.toString(i); + forge.upgradeAndCall(proxies[i], implementationV2, encodeV2Data(data)); + assertEq(forge.implementationOf(proxies[i]), implementationV2); + assertEq(MockImplementationV2(proxies[i]).getData(), data); + } + } + + function test_revoke() public { + address proxy = forge.deploy(implementationV1, alice); + address admin = forge.adminOf(proxy); + assertEq(getProxyAdminOwner(admin), address(forge)); + assertEq(forge.implementationOf(proxy), implementationV1); + assertEq(forge.ownerOf(proxy), alice); + + vm.expectEmit(true, true, true, true, address(forge)); + emit IProxyForge.ProxyRevoked(proxy); + + vm.prank(alice); + forge.revoke(proxy); + assertEq(getProxyAdminOwner(admin), alice); + assertEq(forge.implementationOf(proxy), address(0)); + assertEq(forge.ownerOf(proxy), address(0)); + } + + function test_revoke_revertsWithUnauthorizedAccount() public { + address proxy = forge.deploy(implementationV1, alice); + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); + forge.revoke(proxy); + } + + function test_changeOwner() public { + address proxy = forge.deploy(implementationV1, alice); + + vm.expectEmit(true, true, true, true, address(forge)); + emit IProxyForge.ProxyOwnerChanged(proxy, bob); + + vm.startPrank(alice); + forge.changeOwner(proxy, bob); + assertEq(forge.ownerOf(proxy), bob); + + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, alice)); + forge.upgrade(proxy, implementationV2); + vm.stopPrank(); + + expectEmitUpgradeEvents(proxy, implementationV2); + vm.prank(bob); + forge.upgrade(proxy, implementationV2); + } + + function test_changeOwner_revertsWithUnauthorizedAccount() public { + address proxy = forge.deploy(implementationV1, alice); + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, bob)); + vm.prank(bob); + forge.changeOwner(proxy, bob); + } + + function test_changeOwner_revertsWithInvalidProxyOwner() public { + address proxy = forge.deploy(implementationV1, address(this)); + vm.expectRevert(IProxyForge.InvalidProxyOwner.selector); + forge.changeOwner(proxy, address(0)); + } + + function test_adminOf() public { + address proxy = forge.deploy(implementationV1, alice); + address admin = forge.adminOf(proxy); + assertEq(admin, getProxyAdmin(proxy)); + assertEq(admin, vm.computeCreateAddress(proxy, uint256(1))); + } + + function test_implementationOf() public { + address proxy = forge.deploy(implementationV1, alice); + assertEq(getProxyImplementation(proxy), implementationV1); + assertEq(forge.implementationOf(proxy), implementationV1); + } + + function test_ownerOf() public { + address proxy = forge.deploy(implementationV1, alice); + assertEq(forge.ownerOf(proxy), alice); + } + + function test_computeProxyAddress_CREATE() public { + address predicted = forge.computeProxyAddress(vm.getNonce(address(forge))); + address deployed = forge.deploy(implementationV1, alice); + assertEq(deployed, predicted); + } + + function test_computeProxyAddress_CREATE2() public { + bytes32 salt = generateSalt(address(this)); + bytes memory data = encodeV1Data(100); + + address predicted = forge.computeProxyAddress(implementationV1, salt, data); + address deployed = forge.deployDeterministicAndCall(implementationV1, alice, salt, data); + assertEq(deployed, predicted); + } + + function test_integration_completeLifecycle() public { + address proxy = forge.deployAndCall(implementationV1, alice, encodeV1Data(100)); + address admin = forge.adminOf(proxy); + + MockImplementationV1 mockProxyV1 = MockImplementationV1(proxy); + assertEq(mockProxyV1.version(), "1"); + assertEq(mockProxyV1.getValue(), 100); + assertEq(mockProxyV1.setValue(200), 200); + + vm.expectRevert(ForgeProxy.ProxyDeniedAdminAccess.selector); + vm.prank(admin); + mockProxyV1.getValue(); + + bytes memory data = encodeV2Data("ProxyForge"); + + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); + forge.upgradeAndCall(proxy, implementationV2, data); + + vm.prank(alice); + forge.upgradeAndCall(proxy, implementationV2, data); + assertEq(forge.implementationOf(proxy), implementationV2); + + MockImplementationV2 mockProxyV2 = MockImplementationV2(proxy); + assertEq(mockProxyV2.version(), "2"); + assertEq(mockProxyV2.getValue(), 200); + assertEq(mockProxyV2.getData(), "ProxyForge"); + + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); + forge.changeOwner(proxy, bob); + + vm.prank(alice); + forge.changeOwner(proxy, bob); + assertEq(forge.ownerOf(proxy), bob); + + vm.prank(bob); + forge.upgrade(proxy, implementationV1); + assertEq(forge.implementationOf(proxy), implementationV1); + } } diff --git a/test/proxy/ForgeProxy.t.sol b/test/proxy/ForgeProxy.t.sol index d4971d1..b34f7b9 100644 --- a/test/proxy/ForgeProxy.t.sol +++ b/test/proxy/ForgeProxy.t.sol @@ -8,198 +8,198 @@ import {MockImplementationV1, MockImplementationV2} from "test/shared/mocks/Mock import {BaseTest} from "test/shared/BaseTest.sol"; contract ForgeProxyTest is BaseTest { - address internal admin; - address internal proxy; - - function setUp() public virtual override { - super.setUp(); - - proxy = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); - admin = vm.computeCreateAddress(proxy, uint256(1)); - - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV1); - emit ForgeProxy.AdminChanged(address(0), admin); - - vm.expectEmit(true, true, true, true, admin); - emit ForgeProxyAdmin.OwnershipTransferred(address(0), alice); - - bytes memory data = abi.encodeCall(MockImplementationV1.initialize, (abi.encode(100))); - proxy = address(new ForgeProxy(implementationV1, alice, data)); - } - - function test_constructor() public view { - assertEq(getProxyImplementation(proxy), implementationV1); - assertEq(getProxyAdmin(proxy), admin); - assertEq(IForgeProxyAdmin(admin).owner(), alice); - - MockImplementationV1 mockProxy = MockImplementationV1(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "1"); - assertEq(mockProxy.getValue(), 100); - } - - function test_constructor_withoutInitialization() public { - proxy = address(new ForgeProxy(implementationV1, alice, "")); - admin = vm.computeCreateAddress(proxy, uint256(1)); - - assertEq(getProxyImplementation(proxy), implementationV1); - assertEq(getProxyAdmin(proxy), admin); - assertEq(getProxyAdminOwner(admin), alice); - - MockImplementationV1 mockProxy = MockImplementationV1(proxy); - assertFalse(mockProxy.initialized()); - assertEq(mockProxy.version(), "1"); - assertEq(mockProxy.getValue(), uint256(0)); - } - - function test_constructor_revertsWithInvalidImplementation() public { - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - new ForgeProxy(address(0), alice, ""); - - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - new ForgeProxy(address(0xdeadbeef), alice, ""); - } - - function test_constructor_revertsWithNonPayable() public { - vm.expectRevert(ForgeProxy.NonPayable.selector); - new ForgeProxy{value: 1 ether}(implementationV1, alice, ""); - } - - function test_fallback_revertsWithProxyDeniedAdminAccess() public { - vm.startPrank(admin); - vm.expectRevert(ForgeProxy.ProxyDeniedAdminAccess.selector); - MockImplementationV1(proxy).setValue(200); - - vm.expectRevert(ForgeProxy.ProxyDeniedAdminAccess.selector); - (bool success, ) = proxy.call(abi.encodeCall(MockImplementationV1.getValue, ())); - assertTrue(success); - - vm.stopPrank(); - } - - function test_fallback() public { - assertEq(MockImplementationV1(proxy).setValue(300), 300); - } - - function test_fallback_payable() public { - vm.expectEmit(true, true, true, true, proxy); - emit MockImplementationV1.Deposited(address(this), 2 ether); - - MockImplementationV1(proxy).deposit{value: 2 ether}(); - assertEq(proxy.balance, 2 ether); - } - - function test_upgrade_withoutInitialization() public { - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV2); - - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, ""); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getValue(), 100); - } - - function test_upgrade_withInitialization() public { - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV2); - - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, encodeV2Data("ProxyForge")); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getValue(), 100); - assertEq(mockProxy.getData(), "ProxyForge"); - } - - function test_upgrade_withInitializationAndValue() public { - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV2); - - vm.deal(alice, 10 ether); - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall{value: 10 ether}(proxy, implementationV2, encodeV2Data("ProxyForge")); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getValue(), 100); - assertEq(mockProxy.getData(), "ProxyForge"); - assertEq(proxy.balance, 10 ether); - } - - function test_upgrade_multipleUpgrades() public { - MockImplementationV1 mockProxy = MockImplementationV1(proxy); - mockProxy.deposit{value: 5 ether}(); - - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV2); - - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, ""); - assertEq(getProxyImplementation(proxy), implementationV2); - - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV1); - - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV1, ""); - assertEq(getProxyImplementation(proxy), implementationV1); - - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.getValue(), 100); - assertEq(proxy.balance, 5 ether); - } - - function test_upgrade_withLargeData() public { - string memory data = ""; - for (uint i = 0; i < 32; ++i) { - data = string.concat(data, "This is a test string for large calldata handling in proxy contracts. "); - } - - MockImplementationV1 mockProxy = MockImplementationV1(proxy); - mockProxy.setValue(999); - assertEq(mockProxy.getValue(), 999); - - mockProxy.setValue(1000); - assertEq(mockProxy.getValue(), 1000); - - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV2); - - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, encodeV2Data(data)); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxyV2 = MockImplementationV2(proxy); - assertEq(mockProxyV2.getData(), data); - } - - function test_upgrade_revertsWithInvalidImplementation() public impersonate(alice) { - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, address(0), ""); - - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, address(0xdeadbeef), ""); - } - - function test_upgrade_revertsWithNonPayable() public { - vm.expectRevert(ForgeProxy.NonPayable.selector); - vm.deal(alice, 10 ether); - vm.prank(alice); - IForgeProxyAdmin(admin).upgradeAndCall{value: 10 ether}(proxy, implementationV2, ""); - } - - function test_upgrade_revertsWithUnauthorizedAccount() public { - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); - IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, ""); - } + address internal admin; + address internal proxy; + + function setUp() public virtual override { + super.setUp(); + + proxy = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + admin = vm.computeCreateAddress(proxy, uint256(1)); + + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV1); + emit ForgeProxy.AdminChanged(address(0), admin); + + vm.expectEmit(true, true, true, true, admin); + emit ForgeProxyAdmin.OwnershipTransferred(address(0), alice); + + bytes memory data = abi.encodeCall(MockImplementationV1.initialize, (abi.encode(100))); + proxy = address(new ForgeProxy(implementationV1, alice, data)); + } + + function test_constructor() public view { + assertEq(getProxyImplementation(proxy), implementationV1); + assertEq(getProxyAdmin(proxy), admin); + assertEq(IForgeProxyAdmin(admin).owner(), alice); + + MockImplementationV1 mockProxy = MockImplementationV1(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "1"); + assertEq(mockProxy.getValue(), 100); + } + + function test_constructor_withoutInitialization() public { + proxy = address(new ForgeProxy(implementationV1, alice, "")); + admin = vm.computeCreateAddress(proxy, uint256(1)); + + assertEq(getProxyImplementation(proxy), implementationV1); + assertEq(getProxyAdmin(proxy), admin); + assertEq(getProxyAdminOwner(admin), alice); + + MockImplementationV1 mockProxy = MockImplementationV1(proxy); + assertFalse(mockProxy.initialized()); + assertEq(mockProxy.version(), "1"); + assertEq(mockProxy.getValue(), uint256(0)); + } + + function test_constructor_revertsWithInvalidImplementation() public { + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + new ForgeProxy(address(0), alice, ""); + + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + new ForgeProxy(address(0xdeadbeef), alice, ""); + } + + function test_constructor_revertsWithNonPayable() public { + vm.expectRevert(ForgeProxy.NonPayable.selector); + new ForgeProxy{value: 1 ether}(implementationV1, alice, ""); + } + + function test_fallback_revertsWithProxyDeniedAdminAccess() public { + vm.startPrank(admin); + vm.expectRevert(ForgeProxy.ProxyDeniedAdminAccess.selector); + MockImplementationV1(proxy).setValue(200); + + vm.expectRevert(ForgeProxy.ProxyDeniedAdminAccess.selector); + (bool success,) = proxy.call(abi.encodeCall(MockImplementationV1.getValue, ())); + assertTrue(success); + + vm.stopPrank(); + } + + function test_fallback() public { + assertEq(MockImplementationV1(proxy).setValue(300), 300); + } + + function test_fallback_payable() public { + vm.expectEmit(true, true, true, true, proxy); + emit MockImplementationV1.Deposited(address(this), 2 ether); + + MockImplementationV1(proxy).deposit{value: 2 ether}(); + assertEq(proxy.balance, 2 ether); + } + + function test_upgrade_withoutInitialization() public { + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV2); + + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, ""); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getValue(), 100); + } + + function test_upgrade_withInitialization() public { + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV2); + + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, encodeV2Data("ProxyForge")); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getValue(), 100); + assertEq(mockProxy.getData(), "ProxyForge"); + } + + function test_upgrade_withInitializationAndValue() public { + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV2); + + vm.deal(alice, 10 ether); + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall{value: 10 ether}(proxy, implementationV2, encodeV2Data("ProxyForge")); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getValue(), 100); + assertEq(mockProxy.getData(), "ProxyForge"); + assertEq(proxy.balance, 10 ether); + } + + function test_upgrade_multipleUpgrades() public { + MockImplementationV1 mockProxy = MockImplementationV1(proxy); + mockProxy.deposit{value: 5 ether}(); + + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV2); + + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, ""); + assertEq(getProxyImplementation(proxy), implementationV2); + + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV1); + + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV1, ""); + assertEq(getProxyImplementation(proxy), implementationV1); + + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.getValue(), 100); + assertEq(proxy.balance, 5 ether); + } + + function test_upgrade_withLargeData() public { + string memory data = ""; + for (uint256 i = 0; i < 32; ++i) { + data = string.concat(data, "This is a test string for large calldata handling in proxy contracts. "); + } + + MockImplementationV1 mockProxy = MockImplementationV1(proxy); + mockProxy.setValue(999); + assertEq(mockProxy.getValue(), 999); + + mockProxy.setValue(1000); + assertEq(mockProxy.getValue(), 1000); + + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV2); + + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, encodeV2Data(data)); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxyV2 = MockImplementationV2(proxy); + assertEq(mockProxyV2.getData(), data); + } + + function test_upgrade_revertsWithInvalidImplementation() public impersonate(alice) { + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, address(0), ""); + + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, address(0xdeadbeef), ""); + } + + function test_upgrade_revertsWithNonPayable() public { + vm.expectRevert(ForgeProxy.NonPayable.selector); + vm.deal(alice, 10 ether); + vm.prank(alice); + IForgeProxyAdmin(admin).upgradeAndCall{value: 10 ether}(proxy, implementationV2, ""); + } + + function test_upgrade_revertsWithUnauthorizedAccount() public { + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); + IForgeProxyAdmin(admin).upgradeAndCall(proxy, implementationV2, ""); + } } diff --git a/test/proxy/ForgeProxyAdmin.t.sol b/test/proxy/ForgeProxyAdmin.t.sol index d199f4d..76dbe7d 100644 --- a/test/proxy/ForgeProxyAdmin.t.sol +++ b/test/proxy/ForgeProxyAdmin.t.sol @@ -8,129 +8,129 @@ import {MockImplementationV1, MockImplementationV2} from "test/shared/mocks/Mock import {BaseTest} from "test/shared/BaseTest.sol"; contract ForgeProxyAdminTest is BaseTest { - address internal immutable newOwner = makeAddr("newOwner"); - address internal immutable invalidOwner = makeAddr("invalidOwner"); - - IForgeProxyAdmin internal admin; - address internal proxy; - - function setUp() public virtual override { - super.setUp(); - - proxy = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); - admin = IForgeProxyAdmin(vm.computeCreateAddress(proxy, uint256(1))); - - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementationV1); - emit ForgeProxy.AdminChanged(address(0), address(admin)); - - vm.expectEmit(true, true, true, true, address(admin)); - emit ForgeProxyAdmin.OwnershipTransferred(address(0), address(this)); - - bytes memory data = abi.encodeCall(MockImplementationV1.initialize, (abi.encode(100))); - proxy = address(new ForgeProxy(implementationV1, address(this), data)); - } - - function test_constructor() public view { - assertEq(getProxyAdminOwner(address(admin)), address(this)); - assertEq(admin.owner(), address(this)); - assertEq(admin.UPGRADE_INTERFACE_VERSION(), "5.0.0"); - } - - function test_constructor_revertsWithInvalidNewOwner() public { - vm.expectRevert(ForgeProxyAdmin.InvalidNewOwner.selector); - new ForgeProxyAdmin(address(0)); - } - - function test_transferOwnership() public { - vm.expectEmit(true, true, true, true); - emit ForgeProxyAdmin.OwnershipTransferred(address(this), newOwner); - - admin.transferOwnership(newOwner); - assertEq(admin.owner(), newOwner); - } - - function test_transferOwnership_revertsWithInvalidNewOwner() public { - vm.expectRevert(ForgeProxyAdmin.InvalidNewOwner.selector); - admin.transferOwnership(address(0)); - } - - function test_transferOwnership_revertsWithUnauthorizedAccount() public { - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, invalidOwner)); - vm.prank(invalidOwner); - admin.transferOwnership(newOwner); - } - - function test_upgradeAndCall_withoutInitialization() public { - admin.upgradeAndCall(proxy, implementationV2, ""); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getValue(), 100); - } - - function test_upgradeAndCall_withInitialization() public { - bytes memory data = abi.encodeCall(MockImplementationV2.initialize, (abi.encode("ProxyForge"))); - admin.upgradeAndCall(proxy, implementationV2, data); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getValue(), 100); - assertEq(mockProxy.getData(), "ProxyForge"); - } - - function test_upgradeAndCall_withInitializationAndValue() public { - bytes memory data = abi.encodeCall(MockImplementationV2.initialize, (abi.encode("ProxyForge"))); - admin.upgradeAndCall{value: 1 ether}(proxy, implementationV2, data); - assertEq(getProxyImplementation(proxy), implementationV2); - - MockImplementationV2 mockProxy = MockImplementationV2(proxy); - assertTrue(mockProxy.initialized()); - assertEq(mockProxy.version(), "2"); - assertEq(mockProxy.getValue(), 100); - assertEq(mockProxy.getData(), "ProxyForge"); - assertEq(proxy.balance, 1 ether); - } - - function test_upgradeAndCall_ownershipTransferAffectsProxies() public { - admin.transferOwnership(newOwner); - assertEq(admin.owner(), newOwner); - - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); - admin.upgradeAndCall(proxy, implementationV2, ""); - - vm.prank(newOwner); - admin.upgradeAndCall(proxy, implementationV2, ""); - assertEq(getProxyImplementation(proxy), implementationV2); - } - - function test_upgradeAndCall_revertsWithUnauthorizedAccount() public { - vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, invalidOwner)); - vm.prank(invalidOwner); - admin.upgradeAndCall(proxy, implementationV2, ""); - } - - function test_upgradeAndCall_revertsWithInvalidImplementation() public { - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - admin.upgradeAndCall(proxy, address(0), ""); - - vm.expectRevert(ForgeProxy.InvalidImplementation.selector); - admin.upgradeAndCall(proxy, address(0xdeadbeef), ""); - } - - function test_fallback_revertsWithInvalidCalldataLength() public { - vm.expectRevert(ForgeProxyAdmin.InvalidCalldataLength.selector); - (bool success, ) = address(admin).call(hex"1234"); - assertTrue(success); - } - - function test_fallback_revertsWithInvalidSelector() public { - vm.expectRevert(ForgeProxyAdmin.InvalidSelector.selector); - (bool success, ) = address(admin).call(abi.encodeWithSelector(bytes4(0x12345678))); - assertTrue(success); - } + address internal immutable newOwner = makeAddr("newOwner"); + address internal immutable invalidOwner = makeAddr("invalidOwner"); + + IForgeProxyAdmin internal admin; + address internal proxy; + + function setUp() public virtual override { + super.setUp(); + + proxy = vm.computeCreateAddress(address(this), vm.getNonce(address(this))); + admin = IForgeProxyAdmin(vm.computeCreateAddress(proxy, uint256(1))); + + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementationV1); + emit ForgeProxy.AdminChanged(address(0), address(admin)); + + vm.expectEmit(true, true, true, true, address(admin)); + emit ForgeProxyAdmin.OwnershipTransferred(address(0), address(this)); + + bytes memory data = abi.encodeCall(MockImplementationV1.initialize, (abi.encode(100))); + proxy = address(new ForgeProxy(implementationV1, address(this), data)); + } + + function test_constructor() public view { + assertEq(getProxyAdminOwner(address(admin)), address(this)); + assertEq(admin.owner(), address(this)); + assertEq(admin.UPGRADE_INTERFACE_VERSION(), "5.0.0"); + } + + function test_constructor_revertsWithInvalidNewOwner() public { + vm.expectRevert(ForgeProxyAdmin.InvalidNewOwner.selector); + new ForgeProxyAdmin(address(0)); + } + + function test_transferOwnership() public { + vm.expectEmit(true, true, true, true); + emit ForgeProxyAdmin.OwnershipTransferred(address(this), newOwner); + + admin.transferOwnership(newOwner); + assertEq(admin.owner(), newOwner); + } + + function test_transferOwnership_revertsWithInvalidNewOwner() public { + vm.expectRevert(ForgeProxyAdmin.InvalidNewOwner.selector); + admin.transferOwnership(address(0)); + } + + function test_transferOwnership_revertsWithUnauthorizedAccount() public { + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, invalidOwner)); + vm.prank(invalidOwner); + admin.transferOwnership(newOwner); + } + + function test_upgradeAndCall_withoutInitialization() public { + admin.upgradeAndCall(proxy, implementationV2, ""); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getValue(), 100); + } + + function test_upgradeAndCall_withInitialization() public { + bytes memory data = abi.encodeCall(MockImplementationV2.initialize, (abi.encode("ProxyForge"))); + admin.upgradeAndCall(proxy, implementationV2, data); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getValue(), 100); + assertEq(mockProxy.getData(), "ProxyForge"); + } + + function test_upgradeAndCall_withInitializationAndValue() public { + bytes memory data = abi.encodeCall(MockImplementationV2.initialize, (abi.encode("ProxyForge"))); + admin.upgradeAndCall{value: 1 ether}(proxy, implementationV2, data); + assertEq(getProxyImplementation(proxy), implementationV2); + + MockImplementationV2 mockProxy = MockImplementationV2(proxy); + assertTrue(mockProxy.initialized()); + assertEq(mockProxy.version(), "2"); + assertEq(mockProxy.getValue(), 100); + assertEq(mockProxy.getData(), "ProxyForge"); + assertEq(proxy.balance, 1 ether); + } + + function test_upgradeAndCall_ownershipTransferAffectsProxies() public { + admin.transferOwnership(newOwner); + assertEq(admin.owner(), newOwner); + + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, address(this))); + admin.upgradeAndCall(proxy, implementationV2, ""); + + vm.prank(newOwner); + admin.upgradeAndCall(proxy, implementationV2, ""); + assertEq(getProxyImplementation(proxy), implementationV2); + } + + function test_upgradeAndCall_revertsWithUnauthorizedAccount() public { + vm.expectRevert(abi.encodeWithSelector(ForgeProxyAdmin.UnauthorizedAccount.selector, invalidOwner)); + vm.prank(invalidOwner); + admin.upgradeAndCall(proxy, implementationV2, ""); + } + + function test_upgradeAndCall_revertsWithInvalidImplementation() public { + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + admin.upgradeAndCall(proxy, address(0), ""); + + vm.expectRevert(ForgeProxy.InvalidImplementation.selector); + admin.upgradeAndCall(proxy, address(0xdeadbeef), ""); + } + + function test_fallback_revertsWithInvalidCalldataLength() public { + vm.expectRevert(ForgeProxyAdmin.InvalidCalldataLength.selector); + (bool success,) = address(admin).call(hex"1234"); + assertTrue(success); + } + + function test_fallback_revertsWithInvalidSelector() public { + vm.expectRevert(ForgeProxyAdmin.InvalidSelector.selector); + (bool success,) = address(admin).call(abi.encodeWithSelector(bytes4(0x12345678))); + assertTrue(success); + } } diff --git a/test/shared/BaseTest.sol b/test/shared/BaseTest.sol index cf8b3b7..3862d90 100644 --- a/test/shared/BaseTest.sol +++ b/test/shared/BaseTest.sol @@ -10,117 +10,113 @@ import {ForgeProxyAdmin} from "src/proxy/ForgeProxyAdmin.sol"; import {MockImplementationV1, MockImplementationV2} from "test/shared/mocks/MockImplementation.sol"; abstract contract BaseTest is Test { - bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - bytes32 internal constant OWNER_SLOT = 0x9bc353c4ee8d049c7cb68b79467fc95d9015a8a82334bd0e61ce699e20cb5bd5; - - address internal immutable alice = makeAddr("alice"); - address internal immutable bob = makeAddr("bob"); - - address internal implementationV1; - address internal implementationV2; - - ProxyForge internal forge; - - modifier assumeEOA(address account) { - vm.assume(isEOA(account)); - _; - } - - modifier assumeEOAs(address accountA, address accountB) { - vm.assume(isEOA(accountA)); - vm.assume(isEOA(accountB)); - vm.assume(accountA != accountB); - _; - } - - modifier impersonate(address account) { - vm.startPrank(account); - _; - vm.stopPrank(); - } - - function setUp() public virtual { - implementationV1 = address(new MockImplementationV1()); - implementationV2 = address(new MockImplementationV2()); - } - - function expectEmitDeployEvents( - address proxy, - address admin, - address implementation, - address owner, - bytes32 salt - ) internal { - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementation); - emit ForgeProxy.AdminChanged(address(0), admin); - - vm.expectEmit(true, true, true, true, admin); - emit ForgeProxyAdmin.OwnershipTransferred(address(0), address(forge)); - - vm.expectEmit(true, true, true, true, address(forge)); - emit IProxyForge.ProxyDeployed(proxy, owner, salt); - emit IProxyForge.ProxyUpgraded(proxy, implementation); - emit IProxyForge.ProxyOwnerChanged(proxy, owner); - } - - function expectEmitUpgradeEvents(address proxy, address implementation) internal { - vm.expectEmit(true, true, true, true, proxy); - emit ForgeProxy.Upgraded(implementation); - - vm.expectEmit(true, true, true, true, address(forge)); - emit IProxyForge.ProxyUpgraded(proxy, implementation); - } - - function verifyProxyStates(address proxy, address admin, address implementation, address owner) internal view { - assertTrue(isContract(proxy) && isContract(admin)); - - assertEq(getProxyAdminOwner(admin), address(forge)); - assertEq(IForgeProxyAdmin(admin).owner(), address(forge)); - - assertEq(getProxyAdmin(proxy), admin); - assertEq(forge.adminOf(proxy), admin); - - assertEq(getProxyImplementation(proxy), implementation); - assertEq(forge.implementationOf(proxy), implementation); - - assertEq(forge.ownerOf(proxy), owner); - } - - function getProxyAdmin(address proxy) internal view returns (address) { - return address(uint160(uint256(vm.load(proxy, ADMIN_SLOT)))); - } - - function getProxyImplementation(address proxy) internal view returns (address) { - return address(uint160(uint256(vm.load(proxy, IMPLEMENTATION_SLOT)))); - } - - function getProxyAdminOwner(address admin) internal view returns (address) { - return address(uint160(uint256(vm.load(admin, OWNER_SLOT)))); - } - - function encodeV1Data(uint256 value) internal pure returns (bytes memory) { - return abi.encodeCall(MockImplementationV1.initialize, (abi.encode(value))); - } - - function encodeV2Data(string memory value) internal pure returns (bytes memory) { - return abi.encodeCall(MockImplementationV2.initialize, (abi.encode(value))); - } - - function encodeSalt(address caller, uint96 identifier) internal pure returns (bytes32 salt) { - return bytes32((uint256(uint160(caller)) << 96) | uint256(identifier)); - } - - function generateSalt(address caller) internal returns (bytes32 salt) { - return encodeSalt(caller, uint96(vm.randomUint(type(uint96).min, type(uint96).max))); - } - - function isContract(address target) internal view returns (bool) { - return target.code.length != uint256(0); - } - - function isEOA(address target) internal view returns (bool) { - return target != address(0) && !isContract(target); - } + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant OWNER_SLOT = 0x9bc353c4ee8d049c7cb68b79467fc95d9015a8a82334bd0e61ce699e20cb5bd5; + + address internal immutable alice = makeAddr("alice"); + address internal immutable bob = makeAddr("bob"); + + address internal implementationV1; + address internal implementationV2; + + ProxyForge internal forge; + + modifier assumeEOA(address account) { + vm.assume(isEOA(account)); + _; + } + + modifier assumeEOAs(address accountA, address accountB) { + vm.assume(isEOA(accountA)); + vm.assume(isEOA(accountB)); + vm.assume(accountA != accountB); + _; + } + + modifier impersonate(address account) { + vm.startPrank(account); + _; + vm.stopPrank(); + } + + function setUp() public virtual { + implementationV1 = address(new MockImplementationV1()); + implementationV2 = address(new MockImplementationV2()); + } + + function expectEmitDeployEvents(address proxy, address admin, address implementation, address owner, bytes32 salt) + internal + { + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementation); + emit ForgeProxy.AdminChanged(address(0), admin); + + vm.expectEmit(true, true, true, true, admin); + emit ForgeProxyAdmin.OwnershipTransferred(address(0), address(forge)); + + vm.expectEmit(true, true, true, true, address(forge)); + emit IProxyForge.ProxyDeployed(proxy, owner, salt); + emit IProxyForge.ProxyUpgraded(proxy, implementation); + emit IProxyForge.ProxyOwnerChanged(proxy, owner); + } + + function expectEmitUpgradeEvents(address proxy, address implementation) internal { + vm.expectEmit(true, true, true, true, proxy); + emit ForgeProxy.Upgraded(implementation); + + vm.expectEmit(true, true, true, true, address(forge)); + emit IProxyForge.ProxyUpgraded(proxy, implementation); + } + + function verifyProxyStates(address proxy, address admin, address implementation, address owner) internal view { + assertTrue(isContract(proxy) && isContract(admin)); + + assertEq(getProxyAdminOwner(admin), address(forge)); + assertEq(IForgeProxyAdmin(admin).owner(), address(forge)); + + assertEq(getProxyAdmin(proxy), admin); + assertEq(forge.adminOf(proxy), admin); + + assertEq(getProxyImplementation(proxy), implementation); + assertEq(forge.implementationOf(proxy), implementation); + + assertEq(forge.ownerOf(proxy), owner); + } + + function getProxyAdmin(address proxy) internal view returns (address) { + return address(uint160(uint256(vm.load(proxy, ADMIN_SLOT)))); + } + + function getProxyImplementation(address proxy) internal view returns (address) { + return address(uint160(uint256(vm.load(proxy, IMPLEMENTATION_SLOT)))); + } + + function getProxyAdminOwner(address admin) internal view returns (address) { + return address(uint160(uint256(vm.load(admin, OWNER_SLOT)))); + } + + function encodeV1Data(uint256 value) internal pure returns (bytes memory) { + return abi.encodeCall(MockImplementationV1.initialize, (abi.encode(value))); + } + + function encodeV2Data(string memory value) internal pure returns (bytes memory) { + return abi.encodeCall(MockImplementationV2.initialize, (abi.encode(value))); + } + + function encodeSalt(address caller, uint96 identifier) internal pure returns (bytes32 salt) { + return bytes32((uint256(uint160(caller)) << 96) | uint256(identifier)); + } + + function generateSalt(address caller) internal returns (bytes32 salt) { + return encodeSalt(caller, uint96(vm.randomUint(type(uint96).min, type(uint96).max))); + } + + function isContract(address target) internal view returns (bool) { + return target.code.length != uint256(0); + } + + function isEOA(address target) internal view returns (bool) { + return target != address(0) && !isContract(target); + } } diff --git a/test/shared/mocks/MockImplementation.sol b/test/shared/mocks/MockImplementation.sol index 2eadb09..cfe7438 100644 --- a/test/shared/mocks/MockImplementation.sol +++ b/test/shared/mocks/MockImplementation.sol @@ -2,60 +2,60 @@ pragma solidity ^0.8.30; contract MockImplementationV1 { - error AlreadyInitialized(); + error AlreadyInitialized(); - event Initialized(address indexed msgSender, uint256 indexed msgValue); + event Initialized(address indexed msgSender, uint256 indexed msgValue); - event Deposited(address indexed msgSender, uint256 indexed msgValue); + event Deposited(address indexed msgSender, uint256 indexed msgValue); - bool public initialized; + bool public initialized; - uint256 public value; + uint256 public value; - function initialize(bytes calldata params) external payable virtual { - if (initialized) revert AlreadyInitialized(); - initialized = true; - value = abi.decode(params, (uint256)); - emit Initialized(msg.sender, msg.value); - } + function initialize(bytes calldata params) external payable virtual { + if (initialized) revert AlreadyInitialized(); + initialized = true; + value = abi.decode(params, (uint256)); + emit Initialized(msg.sender, msg.value); + } - function setValue(uint256 newValue) external returns (uint256) { - return value = newValue; - } + function setValue(uint256 newValue) external returns (uint256) { + return value = newValue; + } - function getValue() external view returns (uint256) { - return value; - } + function getValue() external view returns (uint256) { + return value; + } - function deposit() external payable { - emit Deposited(msg.sender, msg.value); - } + function deposit() external payable { + emit Deposited(msg.sender, msg.value); + } - function version() public pure virtual returns (string memory) { - return "1"; - } + function version() public pure virtual returns (string memory) { + return "1"; + } } contract MockImplementationV2 is MockImplementationV1 { - error NotInitialized(); + error NotInitialized(); - string public data; + string public data; - function initialize(bytes calldata params) external payable virtual override { - if (!initialized) revert NotInitialized(); - data = abi.decode(params, (string)); - emit Initialized(msg.sender, msg.value); - } + function initialize(bytes calldata params) external payable virtual override { + if (!initialized) revert NotInitialized(); + data = abi.decode(params, (string)); + emit Initialized(msg.sender, msg.value); + } - function setData(string calldata newData) external returns (string memory) { - return data = newData; - } + function setData(string calldata newData) external returns (string memory) { + return data = newData; + } - function getData() external view returns (string memory) { - return data; - } + function getData() external view returns (string memory) { + return data; + } - function version() public pure virtual override returns (string memory) { - return "2"; - } + function version() public pure virtual override returns (string memory) { + return "2"; + } }