diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index 12ce4c6..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Counter} from "src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index 079350c..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/ERC1967/ERC1967Proxy.sol b/src/ERC1967/ERC1967Proxy.sol new file mode 100644 index 0000000..d202674 --- /dev/null +++ b/src/ERC1967/ERC1967Proxy.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Proxy} from "../Proxy.sol"; +import {ERC1967Utils} from "./ERC1967Utils.sol"; + +/// @title ERC1967Proxy +/// @notice Minimal upgradeable proxy that delegates calls to an implementation stored in the ERC-1967 implementation slot. +/// @author fomoweth +contract ERC1967Proxy is Proxy { + /// @notice Thrown when the proxy is left uninitialized. + error ProxyUninitialized(); + + /// @notice Initializes the proxy with an implementation and optional initializer calldata. + /// @param implementation The address of the initial implementation contract. + /// @param data ABI-encoded initializer calldata, or empty to skip initialization when permitted. + constructor(address implementation, bytes memory data) payable { + if (!_unsafeAllowUninitialized() && data.length == 0) revert ProxyUninitialized(); + ERC1967Utils.upgradeToAndCall(implementation, data); + } + + /// @notice Returns the current implementation used for delegation. + function _implementation() internal view virtual override returns (address) { + return ERC1967Utils.getImplementation(); + } + + /// @notice Returns whether the proxy can be left uninitialized. + function _unsafeAllowUninitialized() internal pure virtual returns (bool) {} +} diff --git a/src/ERC1967/ERC1967Utils.sol b/src/ERC1967/ERC1967Utils.sol new file mode 100644 index 0000000..dcd8002 --- /dev/null +++ b/src/ERC1967/ERC1967Utils.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title ERC1967Utils +/// @notice Library for reading and writing ERC-1967 storage slots and emitting corresponding events for upgradeable proxies. +/// @author fomoweth +library ERC1967Utils { + /// @notice Thrown when the provided implementation address is invalid. + error InvalidImplementation(); + + /// @notice Thrown when the provided admin address is invalid. + error InvalidAdmin(); + + /// @notice Thrown when the provided beacon address is invalid. + error InvalidBeacon(); + + /// @notice Thrown when Ether is sent to an upgrade with no initialization call. + error NonPayable(); + + /// @notice Thrown when the returned UUID does not match expected ERC-1967 slot. + error UnsupportedProxiableUUID(bytes32 slot); + + /// @notice Emitted when the ERC-1967 implementation slot is updated. + event Upgraded(address indexed implementation); + + /// @notice Emitted when the ERC-1967 admin slot is updated. + event AdminChanged(address previousAdmin, address newAdmin); + + /// @notice Emitted when the ERC-1967 beacon slot is updated. + event BeaconUpgraded(address indexed beacon); + + /// @notice Precomputed event topic for {Upgraded}. + /// @dev keccak256(bytes("Upgraded(address)")) + bytes32 internal constant UPGRADED_EVENT_SIGNATURE = + 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b; + + /// @notice Precomputed event topic for {AdminChanged}. + /// @dev keccak256(bytes("AdminChanged(address,address)")) + bytes32 internal constant ADMIN_CHANGED_EVENT_SIGNATURE = + 0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f; + + /// @notice Precomputed event topic for {BeaconUpgraded}. + /// @dev keccak256(bytes("BeaconUpgraded(address)")) + bytes32 internal constant BEACON_UPGRADED_EVENT_SIGNATURE = + 0x1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e; + + /// @notice ERC-1967 storage slot for the implementation address. + /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.implementation"))) - 1) + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /// @notice ERC-1967 storage slot for the admin address. + /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.admin"))) - 1) + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /// @notice ERC-1967 storage slot for the beacon address. + /// @dev bytes32(uint256(keccak256(bytes("eip1967.proxy.beacon"))) - 1) + bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /// @notice Returns the current implementation stored in the ERC-1967 implementation slot. + /// @return implementation The address of the implementation contract. + function getImplementation() internal view returns (address implementation) { + assembly ("memory-safe") { + implementation := sload(IMPLEMENTATION_SLOT) + } + } + + /// @notice Upgrades the proxy implementation and optionally executes an initialization call. + /// @dev Reverts with {InvalidImplementation} if `implementation` has no deployed code. + /// Emits {Upgraded} with `implementation`. + /// @param implementation The address of the new implementation contract. + /// @param data ABI-encoded initializer calldata, or empty to skip the execution. + function upgradeToAndCall(address implementation, bytes memory data) internal { + assembly ("memory-safe") { + implementation := shr(0x60, shl(0x60, implementation)) + + if iszero(extcodesize(implementation)) { + mstore(0x00, 0x68155f9a) // InvalidImplementation() + revert(0x1c, 0x04) + } + + sstore(IMPLEMENTATION_SLOT, implementation) + log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, implementation) + } + + _executeInitialization(implementation, data); + } + + /// @notice Upgrades the proxy implementation via the UUPS pattern with proxiable UUID validation. + /// @dev Reverts with {InvalidImplementation} if `proxiableUUID()` call fails or does not return 32 bytes. + /// Reverts with {UnsupportedProxiableUUID} if returned UUID is not {IMPLEMENTATION_SLOT}. + /// Emits {Upgraded} with `implementation`. + /// @param implementation The address of the new UUPS-compliant implementation contract. + /// @param data ABI-encoded initializer calldata, or empty to skip the execution. + function upgradeToAndCallUUPS(address implementation, bytes memory data) internal { + assembly ("memory-safe") { + implementation := shr(0x60, shl(0x60, implementation)) + + mstore(0x00, 0x52d1902d) // proxiableUUID() + + if iszero(and(eq(returndatasize(), 0x20), staticcall(gas(), implementation, 0x1c, 0x04, 0x20, 0x20))) { + mstore(0x00, 0x68155f9a) // InvalidImplementation() + revert(0x1c, 0x04) + } + + if iszero(eq(mload(0x20), IMPLEMENTATION_SLOT)) { + mstore(0x00, 0x3878d626) // UnsupportedProxiableUUID(bytes32) + revert(0x1c, 0x24) + } + + sstore(IMPLEMENTATION_SLOT, implementation) + log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, implementation) + } + + _executeInitialization(implementation, data); + } + + /// @notice Returns the current admin stored in the ERC-1967 admin slot. + /// @return admin The address of the proxy admin. + function getAdmin() internal view returns (address admin) { + assembly ("memory-safe") { + admin := sload(ADMIN_SLOT) + } + } + + /// @notice Updates the proxy admin to a new address. + /// @dev Reverts with {InvalidAdmin} if `admin` is the zero address. + /// Emits {AdminChanged} with previous admin and new admin. + /// @param admin The address of the new proxy admin. + function changeAdmin(address admin) internal { + assembly ("memory-safe") { + if iszero(shl(0x60, admin)) { + mstore(0x00, 0xb5eba9f0) // InvalidAdmin() + revert(0x1c, 0x04) + } + + admin := shr(0x60, shl(0x60, admin)) + + mstore(0x00, sload(ADMIN_SLOT)) + mstore(0x20, admin) + sstore(ADMIN_SLOT, admin) + log1(0x00, 0x40, ADMIN_CHANGED_EVENT_SIGNATURE) + } + } + + /// @notice Returns the current beacon stored in the ERC-1967 beacon slot. + /// @return beacon The address of the beacon contract. + function getBeacon() internal view returns (address beacon) { + assembly ("memory-safe") { + beacon := sload(BEACON_SLOT) + } + } + + /// @notice Returns the current implementation resolved by beacon via `implementation()`. + /// @dev Reverts with {InvalidBeacon} if the call fails or does not return 32 bytes. + /// @param beacon The address of the beacon contract to query. + /// @return implementation The address of the implementation returned by the `beacon.implementation()`. + function getBeaconImplementation(address beacon) internal view returns (address implementation) { + assembly ("memory-safe") { + mstore(0x00, 0x5c60da1b) // implementation() + + if iszero(and(eq(returndatasize(), 0x20), staticcall(gas(), beacon, 0x1c, 0x04, 0x00, 0x20))) { + mstore(0x00, 0x30740e75) // InvalidBeacon() + revert(0x1c, 0x04) + } + + implementation := mload(0x00) + } + } + + /// @notice Upgrades the beacon and optionally executes an initialization call on its implementation. + /// @dev Reverts with {InvalidBeacon} if `implementation()` call fails or returned implementation + /// has no deployed code. Emits {BeaconUpgraded} with `beacon`. + /// @param beacon The address of the new beacon contract. + /// @param data ABI-encoded initializer calldata, or empty to skip the execution. + function upgradeBeaconToAndCall(address beacon, bytes memory data) internal { + assembly ("memory-safe") { + beacon := shr(0x60, shl(0x60, beacon)) + + mstore(0x00, returndatasize()) + mstore(0x01, 0x5c60da1b) // implementation() + + if iszero(extcodesize(mload(staticcall(gas(), beacon, 0x1d, 0x04, 0x01, 0x20)))) { + mstore(0x01, 0x30740e75) // InvalidBeacon() + revert(0x1d, 0x04) + } + + sstore(BEACON_SLOT, beacon) + log2(codesize(), 0x00, BEACON_UPGRADED_EVENT_SIGNATURE, beacon) + } + + _executeInitialization(getBeaconImplementation(beacon), data); + } + + /// @notice Executes initialization on `implementation` with `data` if provided; otherwise validates that no Ether was sent. + /// @dev Reverts with {NonPayable} if `data` is empty and `msg.value` is nonzero. + /// @param implementation The address of the target for the initialization delegatecall. + /// @param data ABI-encoded initializer calldata, or empty to skip the execution. + function _executeInitialization(address implementation, bytes memory data) private { + assembly ("memory-safe") { + switch mload(data) + case 0x00 { + if callvalue() { + mstore(0x00, 0x6fb1b0e9) // NonPayable() + revert(0x1c, 0x04) + } + } + default { + if iszero(delegatecall(gas(), implementation, add(data, 0x20), mload(data), codesize(), 0x00)) { + let ptr := mload(0x40) + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + } + } + } +} diff --git a/src/ERC1967/beacon/BeaconProxy.sol b/src/ERC1967/beacon/BeaconProxy.sol new file mode 100644 index 0000000..820719f --- /dev/null +++ b/src/ERC1967/beacon/BeaconProxy.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC1967Utils} from "../ERC1967Utils.sol"; +import {Proxy} from "../../Proxy.sol"; + +/// @title BeaconProxy +contract BeaconProxy is Proxy { + uint256 private immutable _beacon; + + constructor(address beacon, bytes memory data) payable { + ERC1967Utils.upgradeBeaconToAndCall(beacon, data); + _beacon = uint256(uint160(beacon)); + } + + /// @notice Returns the beacon. + function _getBeacon() internal view virtual returns (address) { + return address(uint160(_beacon)); + } + + /// @notice Returns the current implementation of the associated beacon. + function _implementation() internal view virtual override returns (address) { + return ERC1967Utils.getBeaconImplementation(_getBeacon()); + } +} + diff --git a/src/ERC1967/beacon/UpgradeableBeacon.sol b/src/ERC1967/beacon/UpgradeableBeacon.sol new file mode 100644 index 0000000..a0599f5 --- /dev/null +++ b/src/ERC1967/beacon/UpgradeableBeacon.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Ownable} from "src/utils/Ownable.sol"; + +/// @title UpgradeableBeacon +contract UpgradeableBeacon is Ownable { + /// @notice Thrown when the provided implementation address for the beacon is invalid. + error InvalidBeaconImplementation(); + + /// @notice Emitted when the implementation returned by the beacon is updated. + event Upgraded(address indexed implementation); + + /// @notice Precomputed event topic for {Upgraded}. + /// @dev keccak256(bytes("Upgraded(address)")) + bytes32 private constant UPGRADED_EVENT_SIGNATURE = + 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b; + + /// @notice Storage slot for the implementation address. + /// @dev uint72(bytes9(keccak256("UPGRADEABLE_BEACON_IMPLEMENTATION_SLOT"))) + uint256 private constant UPGRADEABLE_BEACON_IMPLEMENTATION_SLOT = 0x7adb300363fbbe04c9; + + constructor(address initialImplementation, address initialOwner) { + _setImplementation(initialImplementation); + _initializeOwner(initialOwner); + } + + /// @notice Returns the current implementation. + function implementation() public view virtual returns (address logic) { + assembly ("memory-safe") { + logic := sload(UPGRADEABLE_BEACON_IMPLEMENTATION_SLOT) + } + } + + /// @notice Upgrades the beacon to a new implementation. + function upgradeTo(address newImplementation) public payable virtual onlyOwner { + _setImplementation(newImplementation); + } + + /// @notice Sets the implementation to `newImplementation` for this beacon. + /// @dev Reverts with {InvalidBeaconImplementation} if `newImplementation` + /// is not a deployed contract. Emits {Upgraded} with `newImplementation`. + function _setImplementation(address newImplementation) private { + assembly ("memory-safe") { + newImplementation := shr(0x60, shl(0x60, newImplementation)) + + if iszero(extcodesize(newImplementation)) { + mstore(0x00, 0x7e5aeae6) // InvalidBeaconImplementation() + revert(0x1c, 0x04) + } + + sstore(UPGRADEABLE_BEACON_IMPLEMENTATION_SLOT, newImplementation) + log2(codesize(), 0x00, UPGRADED_EVENT_SIGNATURE, newImplementation) + } + } +} diff --git a/src/ERC1967/transparent/ProxyAdmin.sol b/src/ERC1967/transparent/ProxyAdmin.sol new file mode 100644 index 0000000..921d426 --- /dev/null +++ b/src/ERC1967/transparent/ProxyAdmin.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Ownable} from "src/utils/Ownable.sol"; + +/// @title ProxyAdmin +/// @notice Auxiliary contract responsible for upgrading {TransparentUpgradeableProxy} instance. +contract ProxyAdmin is Ownable { + /// @notice The version of the upgrade interface of the contract. + /// @dev If this getter is missing, both `upgrade(address,address)` and `upgradeAndCall(address,address,bytes)` + /// are present, and `upgrade` must be used if no function should be called, while `upgradeAndCall` will + /// invoke the `receive` function if the third argument is the empty byte string. + /// If the getter returns `"5.0.0"`, only `upgradeAndCall(address,address,bytes)` is present, and the + /// third argument must be the empty byte string if no function should be called, making it impossible to + /// invoke the `receive` function during an upgrade. + string public constant UPGRADE_INTERFACE_VERSION = "5.0.0"; + + constructor(address initialOwner) { + _initializeOwner(initialOwner); + } + + /// @notice Upgrades the proxy to the new implementation, and optionally executes initialization calldata. + /// @param proxy The address of the proxy instance to upgrade. + /// @param implementation The address of the new implementation to set in the proxy. + /// @param data ABI-encoded initializer calldata, or empty to skip execution. + function upgradeAndCall(address proxy, address implementation, bytes calldata data) + public + payable + virtual + onlyOwner + { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(ptr, 0x4f1ef286) // upgradeToAndCall(address,bytes) + mstore(add(ptr, 0x20), shr(0x60, shl(0x60, implementation))) + mstore(add(ptr, 0x40), 0x40) + mstore(add(ptr, 0x60), data.length) + calldatacopy(add(ptr, 0x80), data.offset, data.length) + + let callSize := add(0x64, and(add(data.length, 0x1f), not(0x1f))) + mstore(0x40, add(ptr, callSize)) + + if iszero(call(gas(), proxy, callvalue(), add(ptr, 0x1c), callSize, codesize(), 0x00)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + } + } +} diff --git a/src/ERC1967/transparent/TransparentUpgradeableProxy.sol b/src/ERC1967/transparent/TransparentUpgradeableProxy.sol new file mode 100644 index 0000000..840d482 --- /dev/null +++ b/src/ERC1967/transparent/TransparentUpgradeableProxy.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC1967Utils} from "../ERC1967Utils.sol"; +import {ERC1967Proxy} from "../ERC1967Proxy.sol"; +import {ProxyAdmin} from "./ProxyAdmin.sol"; + +/// @title TransparentUpgradeableProxy +contract TransparentUpgradeableProxy is ERC1967Proxy { + /// @notice Thrown when proxy admin attempts to access implementation functions. + error ProxyDeniedAdminAccess(); + + uint256 private immutable _admin; + + constructor(address implementation, address initialOwner, bytes memory data) + payable + ERC1967Proxy(implementation, data) + { + _admin = uint256(uint160(address(new ProxyAdmin(initialOwner)))); + ERC1967Utils.changeAdmin(_proxyAdmin()); + } + + /// @notice Returns the admin of this proxy. + function _proxyAdmin() internal view virtual returns (address) { + return address(uint160(_admin)); + } + + function _dispatchUpgradeToAndCall() private { + address implementation; + bytes memory data; + + assembly ("memory-safe") { + // 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) + + if iszero(eq(shr(0xe0, calldataload(0x00)), 0x4f1ef286)) { + mstore(0x00, 0xd2b576ec) // ProxyDeniedAdminAccess() + revert(0x1c, 0x04) + } + + implementation := shr(0x60, shl(0x60, calldataload(0x04))) + + data := mload(0x40) + let offset := add(data, 0x20) + let length := calldataload(0x44) + + mstore(data, length) + calldatacopy(offset, 0x64, length) + mstore(0x40, add(offset, and(add(length, 0x1f), not(0x1f)))) + } + + ERC1967Utils.upgradeToAndCall(implementation, data); + } + + /// @dev If `msg.sender` is the admin process the call internally, + /// otherwise transparently fallback to the proxy behavior. + function _fallback() internal virtual override { + if (msg.sender == _proxyAdmin()) { + _dispatchUpgradeToAndCall(); + } else { + super._fallback(); + } + } +} diff --git a/src/ERC1967/uups/UUPSProxy.sol b/src/ERC1967/uups/UUPSProxy.sol new file mode 100644 index 0000000..87f4e7a --- /dev/null +++ b/src/ERC1967/uups/UUPSProxy.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC1967Proxy} from "../ERC1967Proxy.sol"; + +/// @title UUPSProxy +contract UUPSProxy is ERC1967Proxy { + constructor(address implementation, bytes memory data) payable ERC1967Proxy(implementation, data) {} +} diff --git a/src/ERC1967/uups/UUPSUpgradeable.sol b/src/ERC1967/uups/UUPSUpgradeable.sol new file mode 100644 index 0000000..6f02734 --- /dev/null +++ b/src/ERC1967/uups/UUPSUpgradeable.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC1967Utils} from "../ERC1967Utils.sol"; + +/// @title UUPSUpgradeable +/// @notice Abstract contract provides upgradeability mechanism designed for UUPS (Universal Upgradeable Proxy Standard) proxies. +abstract contract UUPSUpgradeable { + /// @notice Thrown when the call is from an unauthorized context. + error UnauthorizedCallContext(); + + /// @notice The original address of this contract + uint256 private immutable __self = uint256(uint160(address(this))); + + /// @notice The version of the upgrade interface of the contract. + /// @dev If this getter is missing, both `upgrade(address,address)` and `upgradeAndCall(address,address,bytes)` + /// are present, and `upgrade` must be used if no function should be called, while `upgradeAndCall` will + /// invoke the `receive` function if the third argument is the empty byte string. + /// If the getter returns `"5.0.0"`, only `upgradeAndCall(address,address,bytes)` is present, and the + /// third argument must be the empty byte string if no function should be called, making it impossible to + /// invoke the `receive` function during an upgrade. + string public constant UPGRADE_INTERFACE_VERSION = "5.0.0"; + + /// @notice Ensures that the execution is being performed through a delegatecall. + modifier onlyProxy() { + _checkProxy(); + _; + } + + /// @notice Ensures that the execution is not being performed through a delegatecall. + modifier notDelegated() { + _checkNotDelegated(); + _; + } + + /// @notice Returns the storage slot used by the implementation. + function proxiableUUID() public view virtual notDelegated returns (bytes32) { + return ERC1967Utils.IMPLEMENTATION_SLOT; + } + + function upgradeToAndCall(address implementation, bytes calldata data) public payable virtual onlyProxy { + _authorizeUpgrade(implementation); + ERC1967Utils.upgradeToAndCallUUPS(implementation, data); + } + + /// @notice Reverts if the execution is not performed via delegatecall. + function _checkProxy() internal view virtual { + uint256 self = __self; + assembly ("memory-safe") { + if eq(self, address()) { + mstore(0x00, 0x9f03a026) // UnauthorizedCallContext() + revert(0x1c, 0x04) + } + } + } + + /// @notice Reverts if the execution is performed via delegatecall. + function _checkNotDelegated() internal view virtual { + uint256 self = __self; + assembly ("memory-safe") { + if iszero(eq(self, address())) { + mstore(0x00, 0x9f03a026) // UnauthorizedCallContext() + revert(0x1c, 0x04) + } + } + } + + /// @notice Ensures `msg.sender` is authorized to upgrade the proxy to `newImplementation`. + /// @dev Called by {upgradeToAndCall}. + function _authorizeUpgrade(address newImplementation) internal virtual; +} diff --git a/src/Proxy.sol b/src/Proxy.sol new file mode 100644 index 0000000..ad7120e --- /dev/null +++ b/src/Proxy.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title Proxy +/// @notice Provides a fallback function that delegates all calls using the EVM instruction `delegatecall`. +abstract contract Proxy { + /// @notice Delegates the current call to `implementation`. + function _delegate(address implementation) internal virtual { + assembly ("memory-safe") { + calldatacopy(0x00, 0x00, calldatasize()) + + let success := delegatecall(gas(), implementation, 0x00, calldatasize(), 0x00, 0x00) + returndatacopy(0x00, 0x00, returndatasize()) + + switch success + case 0x00 { + revert(0x00, returndatasize()) + } + default { + return(0x00, returndatasize()) + } + } + } + + /// @notice Returns the current implementation address. + /// @dev Should be overridden per pattern. + function _implementation() internal view virtual returns (address); + + /// @notice Delegates the current call to the address returned by `_implementation()`. + function _fallback() internal virtual { + _delegate(_implementation()); + } + + /// @notice Fallback function that delegates calls to the current implementation address. + /// @dev Will run if no other function in the contract matches the call data. + fallback() external payable virtual { + _fallback(); + } + + /// @notice Fallback function that delegates calls to the current implementation address. + /// @dev Will run if the call data is empty. + receive() external payable virtual { + _fallback(); + } +} diff --git a/src/utils/Initializable.sol b/src/utils/Initializable.sol new file mode 100644 index 0000000..bd33a33 --- /dev/null +++ b/src/utils/Initializable.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title Initializable +/// @notice Versioned initializable mixin for upgradeable contracts. +/// @author fomoweth +abstract contract Initializable { + /// @notice Thrown when initialization is attempted in an invalid state. + error InvalidInitialization(); + + /// @notice Thrown when a function restricted to the initialization phase is called outside initialization. + error NotInitializing(); + + /// @notice Emitted when the contract is initialized to `version`. + event Initialized(uint64 version); + + /// @notice Precomputed event topic for {Initialized}. + /// @dev keccak256(bytes("Initialized(uint64)")) + bytes32 private constant INITIALIZED_EVENT_SIGNATURE = + 0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2; + + /// @notice Storage slot for the initialization state (initializing flag + initialized version). + /// @dev bytes32(~uint256(uint32(bytes4(keccak256("INITIALIZATION_SLOT"))))) + bytes32 private constant INITIALIZATION_SLOT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff865973bc; + + /// @notice Maximum initializable version number. + /// @dev Sentinel value used by {_disableInitializers} to permanently lock initialization. + uint64 private constant MAX_VERSION = (1 << 64) - 1; + + /// @notice Restricts an initializer function to be invoked at most once (version 1). + /// @dev Reverts with {InvalidInitialization} if the contract is already initialized + /// (except for permitted construction context). + modifier initializer() { + bool isTopLevelCall; + assembly ("memory-safe") { + let state := sload(INITIALIZATION_SLOT) + if state { + if iszero(lt(extcodesize(address()), eq(shr(0x01, state), 0x01))) { + mstore(0x00, 0xf92ee8a9) // InvalidInitialization() + revert(0x1c, 0x04) + } + } + isTopLevelCall := iszero(and(state, 0x01)) + sstore(INITIALIZATION_SLOT, 0x03) + } + _; + assembly ("memory-safe") { + if isTopLevelCall { + sstore(INITIALIZATION_SLOT, 0x02) + mstore(0x20, 0x01) + log1(0x20, 0x20, INITIALIZED_EVENT_SIGNATURE) + } + } + } + + /// @notice Restricts a reinitializer function to be invoked with `version` at most once. + /// @dev Reverts with {InvalidInitialization} if called during initialization phase + /// or if `version` is not greater than current version. + modifier reinitializer(uint64 version) { + assembly ("memory-safe") { + version := shl(0x01, and(version, MAX_VERSION)) + let state := sload(INITIALIZATION_SLOT) + if iszero(lt(and(state, 0x01), lt(state, version))) { + mstore(0x00, 0xf92ee8a9) // InvalidInitialization() + revert(0x1c, 0x04) + } + sstore(INITIALIZATION_SLOT, or(0x01, version)) + } + _; + assembly ("memory-safe") { + sstore(INITIALIZATION_SLOT, version) + mstore(0x20, shr(0x01, version)) + log1(0x20, 0x20, INITIALIZED_EVENT_SIGNATURE) + } + } + + /// @notice Restricts a function to be callable only during an initialization phase. + /// @dev Functions with this modifier may only be invoked within the dynamic call + /// tree of a function guarded by {initializer} or {reinitializer}. + modifier onlyInitializing() { + _checkInitializing(); + _; + } + + /// @notice Checks if the contract is currently initializing. + /// @dev Reverts with {NotInitializing} when the initializing flag is not set. + function _checkInitializing() internal view virtual { + assembly ("memory-safe") { + if iszero(and(0x01, sload(INITIALIZATION_SLOT))) { + mstore(0x00, 0xd7e6bcf8) // NotInitializing() + revert(0x1c, 0x04) + } + } + } + + /// @notice Permanently locks the contract, preventing any future initialization or reinitialization. + /// @dev Sets the initialized version to {MAX_VERSION} and emits {Initialized} with {MAX_VERSION}. + /// Reverts with {InvalidInitialization} if called while initializing. + function _disableInitializers() internal virtual { + assembly ("memory-safe") { + let state := sload(INITIALIZATION_SLOT) + if and(state, 0x01) { + mstore(0x00, 0xf92ee8a9) // InvalidInitialization() + revert(0x1c, 0x04) + } + if iszero(eq(shr(0x01, state), MAX_VERSION)) { + sstore(INITIALIZATION_SLOT, shl(0x01, MAX_VERSION)) + mstore(0x20, MAX_VERSION) + log1(0x20, 0x20, INITIALIZED_EVENT_SIGNATURE) + } + } + } + + /// @notice Returns the highest version that has been initialized. + /// @return version The initialized version. + function _getInitializedVersion() internal view virtual returns (uint64 version) { + assembly ("memory-safe") { + version := shr(0x01, sload(INITIALIZATION_SLOT)) + } + } + + /// @notice Returns whether the contract is currently initializing. + /// @return flag True if the initializing flag is set, false otherwise. + function _isInitializing() internal view virtual returns (bool flag) { + assembly ("memory-safe") { + flag := and(0x01, sload(INITIALIZATION_SLOT)) + } + } +} diff --git a/src/utils/Ownable.sol b/src/utils/Ownable.sol new file mode 100644 index 0000000..074d747 --- /dev/null +++ b/src/utils/Ownable.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title Ownable +/// @notice Authorization mixin that provides basic access control with a single owner. +/// @author fomoweth +abstract contract Ownable { + /// @notice Thrown when owner initialization is attempted more than once. + error AlreadyInitialized(); + + /// @notice Thrown when the provided account address is invalid. + error InvalidAccount(); + + /// @notice Thrown when an unauthorized account attempts a restricted operation. + error Unauthorized(); + + /// @notice Emitted when ownership is transferred from `previousOwner` to `newOwner`. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @notice Precomputed event topic for {OwnershipTransferred}. + /// @dev keccak256(bytes("OwnershipTransferred(address,address)")) + bytes32 private constant OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE = + 0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0; + + /// @notice Storage slot for the owner address. + /// @dev bytes32(~uint256(uint32(bytes4(keccak256("OWNER_SLOT"))))) + bytes32 private constant OWNER_SLOT = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffff9d564fd; + + /// @notice Restricts access to the current owner. + modifier onlyOwner() { + _checkOwner(); + _; + } + + /// @notice Returns the current owner of the contract. + /// @return account The address of the owner. + function owner() public view virtual returns (address account) { + assembly ("memory-safe") { + account := sload(OWNER_SLOT) + } + } + + /// @notice Renounces ownership, leaving the contract without an owner. + function renounceOwnership() public payable virtual onlyOwner { + _transferOwnership(address(0)); + } + + /// @notice Transfers ownership of the contract to a new account. + /// @param account The address of the new owner. + function transferOwnership(address account) public payable virtual onlyOwner { + _checkAccount(account); + _transferOwnership(account); + } + + /// @notice Initializes ownership by setting the initial owner to `account`. + /// @dev Must be called exactly once during construction or initialization. + /// Reverts with {AlreadyInitialized} if an owner has already been set. + function _initializeOwner(address account) internal virtual { + assembly ("memory-safe") { + if sload(OWNER_SLOT) { + mstore(0x00, 0x0dc149f0) // AlreadyInitialized() + revert(0x1c, 0x04) + } + } + _checkAccount(account); + _transferOwnership(account); + } + + /// @notice Sets the owner to `account`. + /// @dev Emits {OwnershipTransferred} with the previous owner and the new owner. + function _transferOwnership(address account) internal virtual { + assembly ("memory-safe") { + account := shr(0x60, shl(0x60, account)) + log3(0x00, 0x00, OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(OWNER_SLOT), account) + sstore(OWNER_SLOT, or(account, shl(0xff, iszero(account)))) + } + } + + /// @notice Validates that `msg.sender` is the current owner. + /// @dev Reverts with {Unauthorized} if `msg.sender` is not the owner. + function _checkOwner() internal view virtual { + assembly ("memory-safe") { + if iszero(eq(caller(), sload(OWNER_SLOT))) { + mstore(0x00, 0x82b42900) // Unauthorized() + revert(0x1c, 0x04) + } + } + } + + /// @notice Validates that `account` is a nonzero address. + /// @dev Reverts with {InvalidAccount} if `account` is the zero address. + function _checkAccount(address account) internal pure virtual { + assembly ("memory-safe") { + if iszero(shl(0x60, account)) { + mstore(0x00, 0x6d187b28) // InvalidAccount() + revert(0x1c, 0x04) + } + } + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 937e8a2..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.24; - -import {Test} from "forge-std/Test.sol"; -import {Counter} from "src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}