├── .gitignore ├── .gitmodules ├── .prettierrc ├── .vscode ├── settings.json └── tasks.json ├── README.md ├── foundry.toml ├── remappings.txt ├── src ├── ERC1967Proxy.sol ├── LibERC1967ProxyWithImmutableArgs.sol ├── UUPSUpgrade.sol └── utils │ ├── proxyCreationCode.sol │ └── utils.sol └── test ├── ERC1967.t.sol ├── UUPSUpgrade.t.sol ├── UUPSUpgradeWithImmutableArgs.t.sol ├── mocks └── MockUUPSUpgrade.sol └── utils └── ProxyTestDeployer.sol /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | broadcast 8 | 9 | json 10 | 11 | #Hardhat files 12 | cache 13 | artifacts 14 | 15 | out 16 | 17 | !.vscode 18 | !.prettierrc 19 | !.gas-snapshot -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/brockelmore/forge-std 4 | [submodule "lib/contracts"] 5 | path = lib/fx-portal 6 | url = https://github.com/fx-portal/contracts 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.compileUsingRemoteVersion": "v0.8.13", 3 | "files.associations": { 4 | ".gas-snapshot": "julia" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | // "command": "forge test --debug Calldata --match-contract Immutable", 7 | "command": "forge test -vvv", 8 | "group": { 9 | "kind": "test", 10 | "isDefault": true 11 | }, 12 | "label": "forge test", 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxies With Immutable Args 2 | 3 | This is a library for deploying ERC1967 proxies (usable with UUPSUpgrade) 4 | that contain "immutable args". 5 | 6 | ## Warning 7 | 8 | Upgradeable proxies containing immutable args might not be the smartest choice if the implementation contract is subject to change. 9 | This work was mostly carried out as an exercise, though the result is fully functional. 10 | One benefit over "clones with immutable args" is that etherscan can detect these contracts as proxies and verify the source. 11 | Though this could likely also be mocked in clones that return the implementation address when `proxiableUUID()` is called. 12 | 13 | ## Contracts 14 | 15 | ```ml 16 | src 17 | ├── ERC1967Proxy.sol - "ERC1967 proxy implementation" 18 | ├── LibERC1967ProxyWithImmutableArgs.sol - "Library for deploying ERC1967 proxy implementation with immutable args" 19 | ├── UUPSUpgrade.sol - "Minimal UUPS upgradeable contract" 20 | └── utils 21 | ├── proxyCreationCode.sol - "Contains helper functions for proxy bytecode creation" 22 | └── utils.sol - "low-level utils" 23 | ``` 24 | 25 | ## Installation 26 | 27 | Install with [Foundry](https://github.com/foundry-rs/foundry) 28 | ```sh 29 | forge install 0xPhaze/proxies-with-immutable-args 30 | ``` 31 | 32 | ## Deploying a proxy with immutable args 33 | 34 | ### Implementation 35 | 36 | The implementation contract, needs inherit from [UUPSUpgrade](./src/UUPSUpgrade.sol). 37 | The `_authorizeUpgrade` function must be overriden (and protected). 38 | 39 | ```solidity 40 | import {OwnableUDS} from "UDS/auth/OwnableUDS.sol"; 41 | import {InitializableUDS} from "UDS/auth/InitializableUDS.sol"; 42 | 43 | import {UUPSUpgrade} from "/UUPSUpgrade.sol"; 44 | 45 | contract Logic is UUPSUpgrade, InitializableUDS, OwnableUDS { 46 | function init() public initializer { 47 | __Ownable_init(); 48 | } 49 | 50 | function _authorizeUpgrade() internal override onlyOwner {} 51 | } 52 | ``` 53 | 54 | The example uses [OwnableUDS](https://github.com/0xPhaze/UDS/blob/master/src/auth/OwnableUDS.sol) and [InitializableUDS](https://github.com/0xPhaze/UDS/blob/master/src/auth/InitializableUDS.sol). 55 | These can be installed via 56 | ```sh 57 | forge install 0xPhaze/UDS 58 | ``` 59 | For more info on upgradeable proxies, have a look at [UDS](https://github.com/0xPhaze/UDS). 60 | 61 | ### Proxy 62 | 63 | To deploy a proxy, call `LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs`. This can be done directly through [Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting) using Foundry, 64 | a `ProxyFactory` contract, or by simply sending the creation code from some account 65 | as a transaction. 66 | The returned address can be casted to the appropriate contract type of the implementation. 67 | 68 | ```solidity 69 | import {LibERC1967ProxyWithImmutableArgs} from "/LibERC1967ProxyWithImmutableArgs.sol"; 70 | 71 | contract ProxyFactory { 72 | function deployProxy( 73 | address implementation, 74 | bytes memory initCalldata, 75 | bytes memory immutableArgs 76 | ) public returns (address) { 77 | return LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs( 78 | implementation, 79 | initCalldata, 80 | immutableArgs 81 | ) 82 | } 83 | } 84 | ``` 85 | 86 | ### Packing and Reading Args 87 | 88 | `immutableArgs` can contain tightly packed arguments. 89 | 90 | ```solidity 91 | address addr = address(0x1337); 92 | uint40 timestamp = 12345; 93 | bytes memory immutableArgs = abi.encodePacked(addr, timestamp); 94 | ``` 95 | 96 | `LibERC1967ProxyWithImmutableArgs` contains helper functions for reading out immutable args. 97 | The offset argument is the bytes position in the tightly packed bytes array. 98 | 99 | ```solidity 100 | contract Logic is UUPSUpgrade { 101 | ... 102 | 103 | function testReadArgs() public { 104 | address addr = LibERC1967ProxyWithImmutableArgs.getArgAddress(0); 105 | uint40 timestamp = LibERC1967ProxyWithImmutableArgs.getArgUint40(20); 106 | 107 | ... 108 | } 109 | } 110 | ``` 111 | 112 | Currently there is no validation for whether this extra calldata is actually present 113 | when trying to read immutable args from calldata. 114 | Attempting to read these from a proxy that doesn't append any, would lead to errors 115 | or incorrect data being returned! 116 | 117 | 118 | ## ERC1967 Specs 119 | 120 | During upgrade & initialization (see [ERC1967Proxy](./src/ERC1967Proxy.sol)): 121 | - implementation code length is checked 122 | - implementation uuid is verified 123 | - `Upgraded` event is emitted 124 | - implementation address is stored in storage slot keccak256("eip1967.proxy.implementation") - 1 as per ERC1967 125 | - a delegatecall is performed on the implementation contract upon proxy initialization if `initCalldata.length != 0` 126 | - call is reverted if not successful and reason is bubbled up 127 | 128 | ## Immutable Args Specs 129 | 130 | For any delegatecall (initialization or proxy call), the "immutable args" are appended to 131 | the calldata, along with 2 extra bytes that signal the length of the immutable data. 132 | This extra calldata generally (unless they explicitly read calldata) does not interfere with the usual control flow of 133 | contracts and can be seen as optional calldata/arguments. 134 | 135 | There is no validation for whether this extra calldata is actually present. 136 | 137 | ## WIP (Work in Progress) 138 | 139 | This work is mostly for fun and for better understanding the EVM. 140 | The goal is to create multiple different versions and test their deployment costs 141 | and extra calldata costs for delegatecalls. 142 | A different version could, for example only read immutable args 143 | through extcodecopy, instead of appending these to calldata. 144 | 145 | Further todos: 146 | - expand test cases 147 | - clean up code 148 | - go play some golf 149 | 150 | ## Disclaimer 151 | 152 | These contracts are a work in progress and should not be used in production. Use at your own risk. 153 | The test cases are meant to pin down proper specification. 154 | Deploying very large immutable args has not been extensively tested 155 | (though the fuzzer didn't complain so far) and might increase gas costs to every when interacting with the proxy in general. 156 | 157 | ## Acknowledgements 158 | - [ClonesWithImmutableArgs](https://github.com/wighawag/clones-with-immutable-args) 159 | - [Foundry](https://github.com/foundry-rs/foundry) -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | solc = "0.8.13" 3 | bytecode_hash = "none" 4 | optimizer_runs = 1000000 5 | 6 | # fuzz_runs = 100_000 7 | # fuzz_max_global_rejects = 0 8 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | /=src/ 2 | UDS/=lib/UDS/src/ 3 | forge-std/=lib/forge-std/src/ -------------------------------------------------------------------------------- /src/ERC1967Proxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | error InvalidUUID(); 5 | error NotAContract(); 6 | 7 | // keccak256("Upgraded(address)") 8 | bytes32 constant UPGRADED_EVENT_SIG = 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b; 9 | 10 | // keccak256("eip1967.proxy.implementation") - 1 11 | bytes32 constant ERC1967_PROXY_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 12 | 13 | /// @notice ERC1967 14 | /// @author phaze (https://github.com/0xPhaze/proxies-with-immutable-args) 15 | abstract contract ERC1967 { 16 | event Upgraded(address indexed implementation); 17 | 18 | function _upgradeToAndCall(address logic, bytes memory data) internal { 19 | assembly { 20 | /// if (logic.code.length == 0) revert NotAContract(); 21 | if iszero(extcodesize(logic)) { 22 | mstore(0, 0x09ee12d5) // NotAContract.selector 23 | revert(28, 4) 24 | } 25 | 26 | /// if (ERC1822(logic).proxiableUUID() != ERC1967_PROXY_STORAGE_SLOT) revert InvalidUUID(); 27 | mstore(0, 0x52d1902d) // proxiableUUID.selector 28 | 29 | let success := call(gas(), logic, 0, 28, 4, 0, 32) 30 | 31 | if iszero(success) { 32 | revert(0, 0) 33 | } 34 | 35 | if iszero(eq(ERC1967_PROXY_STORAGE_SLOT, mload(0))) { 36 | mstore(0, 0x03ed501d) // InvalidUUID.selector 37 | revert(28, 4) 38 | } 39 | 40 | /// emit Upgraded(logic); 41 | log2(0, 0, UPGRADED_EVENT_SIG, logic) 42 | 43 | let data_size := mload(data) 44 | 45 | /// if (data.length != 0) 46 | if data_size { 47 | /// (bool success, bytes memory returndata) = logic.delegatecall(data); 48 | success := delegatecall(gas(), logic, add(data, 0x20), data_size, 0, 0) 49 | 50 | /// if (!success) revert(returndata); 51 | if iszero(success) { 52 | returndatacopy(0, 0, returndatasize()) 53 | revert(0, returndatasize()) 54 | } 55 | } 56 | 57 | /// s().implementation = logic; 58 | sstore(ERC1967_PROXY_STORAGE_SLOT, logic) 59 | } 60 | } 61 | } 62 | 63 | /// @notice Minimal ERC1967Proxy 64 | /// @author phaze (https://github.com/0xPhaze/proxies-with-immutable-args) 65 | contract ERC1967Proxy is ERC1967 { 66 | constructor(address logic, bytes memory data) payable { 67 | _upgradeToAndCall(logic, data); 68 | } 69 | 70 | fallback() external payable { 71 | assembly { 72 | calldatacopy(0, 0, calldatasize()) 73 | 74 | let success := delegatecall(gas(), sload(ERC1967_PROXY_STORAGE_SLOT), 0, calldatasize(), 0, 0) 75 | 76 | returndatacopy(0, 0, returndatasize()) 77 | 78 | if success { 79 | return(0, returndatasize()) 80 | } 81 | 82 | revert(0, returndatasize()) 83 | } 84 | } 85 | } 86 | 87 | /// @notice ERC1822 88 | /// @author phaze (https://github.com/0xPhaze/proxies-with-immutable-args) 89 | abstract contract ERC1822 { 90 | function proxiableUUID() external view virtual returns (bytes32); 91 | } 92 | -------------------------------------------------------------------------------- /src/LibERC1967ProxyWithImmutableArgs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {verifyIsProxiableContract, proxyRuntimeCode, proxyCreationCode} from "./utils/proxyCreationCode.sol"; 5 | 6 | /// @title Library for deploying ERC1967 proxies with immutable args 7 | /// @author phaze (https://github.com/0xPhaze/proxies-with-immutable-args) 8 | /// @notice Inspired by (https://github.com/wighawag/clones-with-immutable-args) 9 | /// @notice The implementation contract can be "read as a proxy" on etherscan 10 | /// @dev Arguments are appended to calldata on any call 11 | library LibERC1967ProxyWithImmutableArgs { 12 | /// @notice Deploys an ERC1967 proxy with immutable bytes args (max. 2^16 - 1 bytes) 13 | /// @notice This contract is not verifiable on etherscan 14 | /// @notice However, the proxy can be marked as a proxy 15 | /// @notice with "Read/Write as a proxy" tabs showing up on etherscan 16 | /// @param implementation address points to the implementation contract 17 | /// @param immutableArgs bytes array of immutable args 18 | /// @return addr address of the deployed proxy 19 | function deployProxyWithImmutableArgs( 20 | address implementation, 21 | bytes memory initCalldata, 22 | bytes memory immutableArgs 23 | ) internal returns (address addr) { 24 | verifyIsProxiableContract(implementation); 25 | 26 | bytes memory runtimeCode = proxyRuntimeCode(immutableArgs); 27 | bytes memory creationCode = proxyCreationCode(implementation, runtimeCode, initCalldata); 28 | 29 | assembly { 30 | addr := create(0, add(creationCode, 0x20), mload(creationCode)) 31 | } 32 | 33 | if (addr.code.length == 0) revert(); 34 | } 35 | 36 | /// @notice Reads a packed immutable arg with as bytes 37 | /// @param argOffset The offset of the arg in the packed data 38 | /// @param argLen The bytes length of the arg in the packed data 39 | /// @return arg The bytes arg value 40 | function getArgBytes(uint256 argOffset, uint256 argLen) internal pure returns (bytes memory arg) { 41 | uint256 offset = getImmutableArgsOffset(); 42 | assembly { 43 | // position arg bytes at free memory position 44 | arg := mload(0x40) 45 | 46 | // store size 47 | mstore(arg, argLen) 48 | 49 | // copy data 50 | calldatacopy(add(arg, 0x20), add(offset, argOffset), argLen) 51 | 52 | // update free memmory pointer 53 | mstore(0x40, add(add(arg, 0x20), argLen)) 54 | } 55 | } 56 | 57 | /// @notice Reads a packed immutable arg with as uint256 58 | /// @param argOffset The offset of the arg in the packed data 59 | /// @param argLen The bytes length of the arg in the packed data 60 | /// @return arg The uint256 arg value 61 | function getArg(uint256 argOffset, uint256 argLen) internal pure returns (uint256 arg) { 62 | assembly { 63 | let sizeOffset := sub(calldatasize(), 2) // offset for size location 64 | let size := shr(240, calldataload(sizeOffset)) // immutableArgs bytes size 65 | let offset := sub(sizeOffset, size) // immutableArgs offset 66 | 67 | // load arg (in 32 bytes) and shift right by (256 - argLen * 8) bytes 68 | arg := shr(shl(3, sub(32, argLen)), calldataload(add(offset, argOffset))) 69 | 70 | // mask if trying to load too much calldata (argOffset + argLen > size) 71 | // should be users responsibility, though this makes testing easier 72 | let overload := shl(3, sub(add(argOffset, argLen), size)) 73 | 74 | if lt(overload, 257) { 75 | arg := shl(overload, shr(overload, arg)) 76 | } 77 | } 78 | } 79 | 80 | /// @notice Reads an immutable arg with type bytes32 81 | /// @param argOffset The offset of the arg in the packed data 82 | /// @return arg The bytes32 arg value 83 | function getArgBytes32(uint256 argOffset) internal pure returns (bytes32 arg) { 84 | return bytes32(getArg(argOffset, 32)); 85 | } 86 | 87 | /// @notice Reads an immutable arg with type address 88 | /// @param argOffset The offset of the arg in the packed data 89 | /// @return arg The address arg value 90 | function getArgAddress(uint256 argOffset) internal pure returns (address arg) { 91 | uint256 offset = getImmutableArgsOffset(); 92 | assembly { 93 | arg := shr(96, calldataload(add(offset, argOffset))) 94 | } 95 | } 96 | 97 | /// @notice Reads an immutable arg with type uint256 98 | /// @param argOffset The offset of the arg in the packed data 99 | /// @return arg The uint256 arg value 100 | function getArgUint256(uint256 argOffset) internal pure returns (uint256 arg) { 101 | return getArg(argOffset, 32); 102 | } 103 | 104 | /// @notice Reads an immutable arg with type uint128 105 | /// @param argOffset The offset of the arg in the packed data 106 | /// @return arg The uint128 arg value 107 | function getArgUint128(uint256 argOffset) internal pure returns (uint128 arg) { 108 | return uint128(getArg(argOffset, 16)); 109 | } 110 | 111 | /// @notice Reads an immutable arg with type uint64 112 | /// @param argOffset The offset of the arg in the packed data 113 | /// @return arg The uint64 arg value 114 | function getArgUint64(uint256 argOffset) internal pure returns (uint64 arg) { 115 | return uint64(getArg(argOffset, 8)); 116 | } 117 | 118 | /// @notice Reads an immutable arg with type uint40 119 | /// @param argOffset The offset of the arg in the packed data 120 | /// @return arg The uint40 arg value 121 | function getArgUint40(uint256 argOffset) internal pure returns (uint40 arg) { 122 | return uint40(getArg(argOffset, 5)); 123 | } 124 | 125 | /// @notice Reads an immutable arg with type uint8 126 | /// @param argOffset The offset of the arg in the packed data 127 | /// @return arg The uint8 arg value 128 | function getArgUint8(uint256 argOffset) internal pure returns (uint8 arg) { 129 | return uint8(getArg(argOffset, 1)); 130 | } 131 | 132 | /// @notice Gets the starting location in bytes in calldata for immutable args 133 | /// @return offset calldata bytes offset for immutable args 134 | function getImmutableArgsOffset() internal pure returns (uint256 offset) { 135 | assembly { 136 | let numBytes := shr(240, calldataload(sub(calldatasize(), 2))) 137 | offset := sub(sub(calldatasize(), 2), numBytes) 138 | } 139 | } 140 | 141 | /// @notice Gets the size in bytes of immutable args 142 | /// @return size bytes size of immutable args 143 | function getImmutableArgsLen() internal pure returns (uint256 size) { 144 | assembly { 145 | size := shr(240, calldataload(sub(calldatasize(), 2))) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/UUPSUpgrade.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC1967, ERC1967_PROXY_STORAGE_SLOT, ERC1822} from "./ERC1967Proxy.sol"; 5 | 6 | // ------------- errors 7 | 8 | error OnlyProxyCallAllowed(); 9 | error DelegateCallNotAllowed(); 10 | 11 | /// @notice Minimal UUPSUpgrade 12 | /// @author phaze (https://github.com/0xPhaze/proxies-with-immutable-args) 13 | abstract contract UUPSUpgrade is ERC1967, ERC1822 { 14 | address private immutable __implementation = address(this); 15 | 16 | /* ------------- external ------------- */ 17 | 18 | function upgradeToAndCall(address logic, bytes calldata data) external { 19 | _authorizeUpgrade(); 20 | _upgradeToAndCall(logic, data); 21 | } 22 | 23 | /* ------------- view ------------- */ 24 | 25 | function proxiableUUID() external view virtual override notDelegated returns (bytes32) { 26 | return ERC1967_PROXY_STORAGE_SLOT; 27 | } 28 | 29 | /* ------------- virtual ------------- */ 30 | 31 | function _authorizeUpgrade() internal virtual; 32 | 33 | /* ------------- modifier ------------- */ 34 | 35 | modifier onlyProxy() { 36 | if (address(this) == __implementation) revert OnlyProxyCallAllowed(); 37 | _; 38 | } 39 | 40 | modifier notDelegated() { 41 | if (address(this) != __implementation) revert DelegateCallNotAllowed(); 42 | _; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/proxyCreationCode.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC1822, ERC1967_PROXY_STORAGE_SLOT, UPGRADED_EVENT_SIG} from "../ERC1967Proxy.sol"; 5 | import {utils} from "./utils.sol"; 6 | 7 | error InvalidUUID(); 8 | error NotAContract(); 9 | error InvalidOffset(uint256 expected, uint256 actual); 10 | error ExceedsMaxArgSize(uint256 size); 11 | 12 | /// @notice Verifies that implementation contract conforms to ERC1967 13 | /// @notice This can be part of creationCode, but doesn't have to be 14 | /// @param implementation address containing the implementation logic 15 | function verifyIsProxiableContract(address implementation) view { 16 | if (implementation.code.length == 0) revert NotAContract(); 17 | 18 | bytes32 uuid = ERC1822(implementation).proxiableUUID(); 19 | 20 | if (uuid != ERC1967_PROXY_STORAGE_SLOT) revert InvalidUUID(); 21 | } 22 | 23 | /// @dev Expects `implementation` (imp) to be on top of the stack 24 | /// @dev Must leave a copy of `implementation` (imp) on top of the stack 25 | /// @param pc program counter, used for calculating offsets 26 | function initCallcode(uint256 pc, bytes memory initCalldata) pure returns (bytes memory code) { 27 | uint256 initCalldataSize = initCalldata.length; 28 | 29 | code = abi.encodePacked( 30 | // push initCalldata to stack in 32 bytes chunks and store in memory at pos 0 31 | 32 | MSTORE(0, initCalldata), // MSTORE icd | | [0, ics) = initcalldata 33 | 34 | /// let success := delegatecall(gas(), implementation, 0, initCalldataSize, 0, 0) 35 | 36 | hex"3d" // RETURNDATASIZE | 00 imp | [0, ics) = initcalldata 37 | hex"3d", // RETURNDATASIZE | 00 00 imp | [0, ics) = initcalldata 38 | PUSHX(initCalldataSize), // PUSHX ics | ics 00 00 imp | [0, ics) = initcalldata 39 | hex"3d" // RETURNDATASIZE | 00 ics 00 00 imp | [0, ics) = initcalldata 40 | hex"84" // DUP5 | imp 00 ics 00 00 imp | [0, ics) = initcalldata 41 | hex"5a" // GAS | gas imp 00 ics 00 00 ..| [0, ics) = initcalldata 42 | hex"f4" // DELEGATECALL | scs imp | ... 43 | ); 44 | 45 | // advance program counter 46 | pc += code.length; 47 | 48 | code = abi.encodePacked(code, 49 | /// if iszero(success) { revert(0, returndatasize()) } 50 | 51 | REVERT_ON_FAILURE(pc) // | imp | ... 52 | ); 53 | } 54 | 55 | /// @notice Returns the creation code for an ERC1967 proxy with immutable args (max. 2^16 - 1 bytes) 56 | /// @param implementation address containing the implementation logic 57 | /// @param runtimeCode evm bytecode (runtime + concatenated extra data) 58 | /// @return creationCode evm bytecode that deploys ERC1967 proxy runtimeCode 59 | function proxyCreationCode( 60 | address implementation, 61 | bytes memory runtimeCode, 62 | bytes memory initCalldata 63 | ) pure returns (bytes memory creationCode) { 64 | uint256 pc; // program counter 65 | 66 | creationCode = abi.encodePacked( 67 | /// log2(0, 0, UPGRADED_EVENT_SIG, logic) 68 | 69 | hex"73", implementation, // PUSH20 imp | imp 70 | hex"80" // DUP1 | imp imp 71 | hex"7f", UPGRADED_EVENT_SIG, // PUSH32 ues | ues imp imp 72 | hex"3d" // RETURNDATASIZE | 00 ues imp imp 73 | hex"3d" // RETURNDATASIZE | 00 00 ues imp imp 74 | hex"a2" // LOG2 | imp 75 | ); 76 | 77 | pc = creationCode.length; 78 | 79 | // optional: insert code to call implementation contract with initCalldata during contract creation 80 | if (initCalldata.length != 0) { 81 | creationCode = abi.encodePacked( 82 | creationCode, 83 | initCallcode(pc, initCalldata) 84 | ); 85 | 86 | // update program counter 87 | pc = creationCode.length; 88 | } 89 | 90 | // PUSH1 is being used for storing runtimeSize 91 | if (runtimeCode.length > type(uint16).max) revert ExceedsMaxArgSize(runtimeCode.length); 92 | 93 | uint16 rts = uint16(runtimeCode.length); 94 | 95 | // runtimeOffset: runtime code location 96 | // = current pc + size of next block 97 | uint16 rto = uint16(pc + 45); 98 | 99 | creationCode = abi.encodePacked( creationCode, 100 | /// sstore(ERC1967_PROXY_STORAGE_SLOT, implementation) 101 | 102 | hex"7f", ERC1967_PROXY_STORAGE_SLOT, // PUSH32 pss | pss imp 103 | hex"55" // SSTORE | 104 | 105 | /// codecopy(0, rto, rts) 106 | 107 | hex"61", rts, // PUSH2 rts | rts 108 | hex"80" // DUP1 | rts rts 109 | hex"61", rto, // PUSH2 rto | rto rts rts 110 | hex"3d" // RETURNDATASIZE | 00 rto rts rts 111 | hex"39" // CODECOPY | rts | [00, rts) = runtimeCode + args 112 | 113 | /// return(0, rts) 114 | 115 | hex"3d" // RETURNDATASIZE | 00 rts 116 | hex"f3" // RETURN 117 | ); // prettier-ignore 118 | 119 | pc = creationCode.length; 120 | 121 | // sanity check for runtime location parameter 122 | if (pc != rto) revert InvalidOffset(rto, creationCode.length); 123 | 124 | creationCode = abi.encodePacked(creationCode, runtimeCode); 125 | } 126 | 127 | /// @notice Returns the runtime code for an ERC1967 proxy with immutable args (max. 2^16 - 1 bytes) 128 | /// @param args immutable args byte array 129 | /// @return runtimeCode evm bytecode 130 | function proxyRuntimeCode(bytes memory args) pure returns (bytes memory runtimeCode) { 131 | uint16 extraDataSize = uint16(args.length) + 2; // 2 extra bytes for the storing the size of the args 132 | 133 | uint8 argsCodeOffset = 0x48; // length of the runtime code 134 | 135 | uint8 returnJumpLocation = argsCodeOffset - 5; 136 | 137 | // @note: uses codecopy, room to optimize by directly encoding immutableArgs into bytecode 138 | runtimeCode = abi.encodePacked( 139 | /// calldatacopy(0, 0, calldatasize()) 140 | 141 | hex"36" // CALLDATASIZE | cds | 142 | hex"3d" // RETURNDATASIZE | 00 cds | 143 | hex"3d" // RETURNDATASIZE | 00 00 cds | 144 | hex"37" // CALLDATACOPY | | [0, cds) = calldata 145 | 146 | /// codecopy(calldatasize(), argsCodeOffset, extraDataSize) 147 | 148 | hex"61", extraDataSize, // PUSH2 xds | xds | [0, cds) = calldata 149 | hex"60", argsCodeOffset, // PUSH1 aco | aco xds | [0, cds) = calldata 150 | hex"36" // CALLDATASIZE | cds aco xds | [0, cds) = calldata 151 | hex"39" // CODECOPY | | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 152 | 153 | /// tcs := add(calldatasize(), extraDataSize) 154 | hex"3d" // RETURNDATASIZE | 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 155 | hex"3d" // RETURNDATASIZE | 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 156 | hex"61", extraDataSize, // PUSH2 xds | xds 00 00 | [0, cds) = calldata 157 | hex"36" // CALLDATASIZE | cds xds 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 158 | hex"01" // ADD | tcs 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 159 | 160 | /// success := delegatecall(gas(), sload(ERC1967_PROXY_STORAGE_SLOT), 0, tcs, 0, 0) 161 | 162 | hex"3d" // RETURNDATASIZE | 00 tcs 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 163 | hex"7f", ERC1967_PROXY_STORAGE_SLOT, // PUSH32 pss | pss 00 tcs 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 164 | hex"54" // SLOAD | pxa 00 tcs 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 165 | hex"5a" // GAS | gas pxa 00 tcs 00 00 | [0, cds) = calldata, [cds, cds + xds] = args + byte2(argSize) 166 | hex"f4" // DELEGATECALL | scs | ... 167 | 168 | /// returndatacopy(0, 0, returndatasize()) 169 | 170 | hex"3d" // RETURNDATASIZE | rds scs | ... 171 | hex"60" hex"00" // PUSH1 00 | 00 rds scs | ... 172 | hex"80" // DUP1 | 00 00 rds scs | ... 173 | hex"3e" // RETURNDATACOPY | scs | [0, rds) = returndata 174 | 175 | /// if success { jump(rtn) } 176 | 177 | hex"60", returnJumpLocation, // PUSH1 rtn | rtn scs | [0, rds) = returndata 178 | hex"57" // JUMPI | | [0, rds) = returndata 179 | 180 | /// revert(0, returndatasize()) 181 | 182 | hex"3d" // RETURNDATASIZE | rds | [0, rds) = returndata 183 | hex"60" hex"00" // PUSH1 00 | 00 rds | [0, rds) = returndata 184 | hex"fd" // REVERT | | 185 | 186 | /// return(0, returndatasize()) 187 | 188 | hex"5b" // JUMPDEST | | [0, rds) = returndata 189 | hex"3d" // RETURNDATASIZE | rds | [0, rds) = returndata 190 | hex"60" hex"00" // PUSH1 00 | 00 rds | [0, rds) = returndata 191 | hex"f3", // RETURN | | ... 192 | 193 | // unreachable code: 194 | // append args and args length for later use 195 | 196 | args, 197 | uint16(args.length) 198 | 199 | ); // prettier-ignore 200 | 201 | // just a sanity check for for jump locations 202 | if (runtimeCode.length != argsCodeOffset + extraDataSize) revert InvalidOffset(argsCodeOffset, runtimeCode.length); 203 | } 204 | 205 | // --------------------------------------------------------------------- 206 | // Snippets 207 | // --------------------------------------------------------------------- 208 | 209 | /// @notice expects call `success` (scs) bool to be on top of stack 210 | function REVERT_ON_FAILURE(uint256 pc) pure returns (bytes memory code) { 211 | // upper bound for when end location requires 32 bytes 212 | uint256 pushNumBytes = utils.getRequiredBytes(pc + 38); 213 | uint256 end = pc + pushNumBytes + 11; 214 | 215 | code = abi.encodePacked( 216 | /// if success { jump(end) } 217 | 218 | PUSHX(end, pushNumBytes), // PUSH1 end | end scs 219 | hex"57" // JUMPI | 220 | 221 | /// returndatacopy(0, 0, returndatasize()) 222 | 223 | hex"3d" // RETURNDATASIZE | rds | ... 224 | hex"60" hex"00" // PUSH1 00 | 00 rds | ... 225 | hex"80" // DUP1 | 00 00 rds | ... 226 | hex"3e" // RETURNDATACOPY | | [0, rds) = returndata 227 | 228 | /// revert(0, returndatasize()) 229 | 230 | hex"3d" // RETURNDATASIZE | rds 231 | hex"60" hex"00" // PUSH1 00 | 00 rds 232 | hex"fd" // REVERT | 233 | 234 | hex"5b" // JUMPDEST | 235 | ); 236 | } 237 | 238 | /// @notice expects call `success` (scs) bool to be on top of stack 239 | /// @notice using this for testing 240 | // apparently you can't revert any returndata 241 | // messages when using CREATE (returndatasize() is always 0) 242 | // that's why I'm encoding it in the returndata 243 | function RETURN_REVERT_REASON_ON_FAILURE_TEST(uint256 pc) pure returns (bytes memory code) { 244 | // upper bound for when end location requires 32 bytes 245 | uint256 pushNumBytes = utils.getRequiredBytes(pc + 38); 246 | 247 | // jump location to continue in code 248 | uint256 end = pc + pushNumBytes + 19; 249 | 250 | code = abi.encodePacked( 251 | /// if success { jump(end) } 252 | 253 | PUSHX(end, pushNumBytes), // PUSH1 end | end scs 254 | hex"57" // JUMPI | 255 | 256 | /// mstore(0, 0) 257 | 258 | hex"60" hex"00" // PUSH1 00 | 00 259 | hex"80" // DUP1 80 | 80 00 260 | hex"52" // MSTORE | | [0] = 00 (STOP opcode; identifier for encoding reverted call reason) 261 | 262 | 263 | /// returndatacopy(1, 0, returndatasize()) 264 | 265 | hex"3d" // RETURNDATASIZE | rds | [0] = 00 266 | hex"60" hex"00" // PUSH1 00 | 00 rds | [0] = 00 267 | hex"60" hex"01" // PUSH1 01 | 01 00 rds | [0] = 00 268 | hex"3e" // RETURNDATACOPY | | [0] = 00 269 | 270 | 271 | /// return(0, 1 + returndatasize()) 272 | 273 | hex"3d" // RETURNDATASIZE | rds | [0, rds + 20) = encoded revert reason 274 | hex"60" hex"01" // PUSH1 01 | 01 rds | [0, rds + 20) = encoded revert reason 275 | hex"01" // ADD | eds | [0, rds + 20) = encoded revert reason 276 | hex"60" hex"00" // PUSH1 00 | 00 eds 277 | hex"f3" // RETURN | 278 | 279 | hex"5b" // JUMPDEST | 280 | ); 281 | 282 | // sanity check 283 | if (end + 1 != pc + code.length) revert InvalidOffset(end + 1, pc + code.length); 284 | } 285 | 286 | 287 | /// @notice Mstore that copies bytes to memory offset in chunks of 32 288 | function MSTORE(uint256 offset, bytes memory data) pure returns (bytes memory code) { 289 | 290 | bytes32[] memory bytes32Data = utils.splitToBytes32(data); 291 | 292 | uint256 numChunks = bytes32Data.length; 293 | 294 | for (uint256 i; i < numChunks; i++) { 295 | code = abi.encodePacked( code, 296 | /// mstore(offset + i * 32, bytes32Data[i]) 297 | 298 | hex"7f", bytes32Data[i], // PUSH32 data | data 299 | PUSHX(offset + i * 32), // PUSHX off | off data 300 | hex"52" // MSTORE | 301 | 302 | ); // prettier-ignore 303 | } 304 | } 305 | 306 | function PUSHX(uint256 value) pure returns (bytes memory code) { 307 | return PUSHX(value, utils.getRequiredBytes(value)); 308 | } 309 | 310 | /// @notice Pushes value with the least required bytes onto stack 311 | /// @notice Probably overkill... 312 | function PUSHX(uint256 value, uint256 numBytes) pure returns (bytes memory code) { 313 | if (numBytes == 1) code = abi.encodePacked(hex"60", uint8(value)); 314 | else if (numBytes == 2) code = abi.encodePacked(hex"61", uint16(value)); 315 | else if (numBytes == 3) code = abi.encodePacked(hex"62", uint24(value)); 316 | else if (numBytes == 4) code = abi.encodePacked(hex"63", uint32(value)); 317 | else if (numBytes == 5) code = abi.encodePacked(hex"64", uint40(value)); 318 | else if (numBytes == 6) code = abi.encodePacked(hex"65", uint48(value)); 319 | else if (numBytes == 7) code = abi.encodePacked(hex"66", uint56(value)); 320 | else if (numBytes == 8) code = abi.encodePacked(hex"67", uint64(value)); 321 | else if (numBytes == 9) code = abi.encodePacked(hex"68", uint72(value)); 322 | else if (numBytes == 10) code = abi.encodePacked(hex"69", uint80(value)); 323 | else if (numBytes == 11) code = abi.encodePacked(hex"6a", uint88(value)); 324 | else if (numBytes == 12) code = abi.encodePacked(hex"6b", uint96(value)); 325 | else if (numBytes == 13) code = abi.encodePacked(hex"6c", uint104(value)); 326 | else if (numBytes == 14) code = abi.encodePacked(hex"6d", uint112(value)); 327 | else if (numBytes == 15) code = abi.encodePacked(hex"6e", uint120(value)); 328 | else if (numBytes == 16) code = abi.encodePacked(hex"6f", uint128(value)); 329 | else if (numBytes == 17) code = abi.encodePacked(hex"70", uint136(value)); 330 | else if (numBytes == 18) code = abi.encodePacked(hex"71", uint144(value)); 331 | else if (numBytes == 19) code = abi.encodePacked(hex"72", uint152(value)); 332 | else if (numBytes == 20) code = abi.encodePacked(hex"73", uint160(value)); 333 | else if (numBytes == 21) code = abi.encodePacked(hex"74", uint168(value)); 334 | else if (numBytes == 22) code = abi.encodePacked(hex"75", uint176(value)); 335 | else if (numBytes == 23) code = abi.encodePacked(hex"76", uint184(value)); 336 | else if (numBytes == 24) code = abi.encodePacked(hex"77", uint192(value)); 337 | else if (numBytes == 25) code = abi.encodePacked(hex"78", uint200(value)); 338 | else if (numBytes == 26) code = abi.encodePacked(hex"79", uint208(value)); 339 | else if (numBytes == 27) code = abi.encodePacked(hex"7a", uint216(value)); 340 | else if (numBytes == 28) code = abi.encodePacked(hex"7b", uint224(value)); 341 | else if (numBytes == 29) code = abi.encodePacked(hex"7c", uint232(value)); 342 | else if (numBytes == 30) code = abi.encodePacked(hex"7d", uint240(value)); 343 | else if (numBytes == 31) code = abi.encodePacked(hex"7e", uint248(value)); 344 | else if (numBytes == 32) code = abi.encodePacked(hex"7f", uint256(value)); 345 | } 346 | -------------------------------------------------------------------------------- /src/utils/utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | library utils { 5 | /// @notice split data to chunks of 32 bytes 6 | function splitToBytes32(bytes memory data) internal pure returns (bytes32[] memory split) { 7 | uint256 numEl = (data.length + 31) >> 5; 8 | 9 | split = new bytes32[](numEl); 10 | 11 | uint256 loc; 12 | 13 | assembly { 14 | loc := add(split, 32) 15 | } 16 | 17 | mstore(loc, data); 18 | } 19 | 20 | /// @notice stores data at offset while preserving existing memory 21 | function mstore(uint256 offset, bytes memory data) internal pure { 22 | uint256 slot; 23 | 24 | uint256 size = data.length; 25 | 26 | uint256 lastFullSlot = size >> 5; 27 | 28 | for (; slot < lastFullSlot; slot++) { 29 | assembly { 30 | let rel_ptr := mul(slot, 32) 31 | let chunk := mload(add(add(data, 32), rel_ptr)) 32 | mstore(add(offset, rel_ptr), chunk) 33 | } 34 | } 35 | 36 | assembly { 37 | let mask := shr(shl(3, and(size, 31)), sub(0, 1)) 38 | let rel_ptr := mul(slot, 32) 39 | let chunk := mload(add(add(data, 32), rel_ptr)) 40 | let prev_data := mload(add(offset, rel_ptr)) 41 | mstore(add(offset, rel_ptr), or(and(chunk, not(mask)), and(prev_data, mask))) 42 | } 43 | } 44 | 45 | /// @notice gets minimum required bytes to store value 46 | function getRequiredBytes(uint256 value) internal pure returns (uint256) { 47 | uint256 numBytes = 1; 48 | 49 | for (; numBytes < 32; ++numBytes) { 50 | value = value >> 8; 51 | if (value == 0) break; 52 | } 53 | 54 | return numBytes; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/ERC1967.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | import {MockUUPSUpgrade} from "./mocks/MockUUPSUpgrade.sol"; 7 | 8 | import "/ERC1967Proxy.sol"; 9 | 10 | // --------------------------------------------------------------------- 11 | // Mock Logic 12 | // --------------------------------------------------------------------- 13 | 14 | error RevertOnInit(); 15 | 16 | contract Logic is MockUUPSUpgrade { 17 | uint256 public initializedCount; 18 | 19 | function init() public { 20 | ++initializedCount; 21 | } 22 | 23 | function initReverts() public pure { 24 | revert RevertOnInit(); 25 | } 26 | 27 | function initRevertsCustomMessage(string memory message) public pure { 28 | require(false, message); 29 | } 30 | } 31 | 32 | contract LogicNonexistentUUID {} 33 | 34 | contract LogicInvalidUUID { 35 | bytes32 public proxiableUUID = bytes32(uint256(0x1234)); 36 | } 37 | 38 | // --------------------------------------------------------------------- 39 | // ERC1967 Tests 40 | // --------------------------------------------------------------------- 41 | 42 | contract TestERC1967 is Test { 43 | event Upgraded(address indexed implementation); 44 | 45 | Logic logic; 46 | address proxy; 47 | 48 | function deployProxyAndCall(address implementation, bytes memory initCalldata) internal virtual returns (address) { 49 | return address(new ERC1967Proxy(address(implementation), initCalldata)); 50 | } 51 | 52 | function setUp() public virtual { 53 | logic = new Logic(); 54 | } 55 | 56 | function test_setUp() public virtual { 57 | assertEq(UPGRADED_EVENT_SIG, keccak256("Upgraded(address)")); 58 | assertEq(ERC1967_PROXY_STORAGE_SLOT, bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); 59 | } 60 | 61 | /* ------------- deployProxyAndCall() ------------- */ 62 | 63 | /// expect implementation to be stored correctly 64 | function test_deployProxyAndCall_implementation() public { 65 | proxy = deployProxyAndCall(address(logic), ""); 66 | 67 | // make sure that implementation is not 68 | // located in sequential storage slot 69 | MockUUPSUpgrade(proxy).scrambleStorage(0, 100); 70 | 71 | assertEq(MockUUPSUpgrade(proxy).implementation(), address(logic)); 72 | assertEq(vm.load(proxy, ERC1967_PROXY_STORAGE_SLOT), bytes32(uint256(uint160(address(logic))))); 73 | } 74 | 75 | /// expect Upgraded(address) to be emitted 76 | function test_deployProxyAndCall_emit() public { 77 | vm.expectEmit(true, false, false, false); 78 | 79 | emit Upgraded(address(logic)); 80 | 81 | proxy = deployProxyAndCall(address(logic), ""); 82 | } 83 | 84 | /// expect proxy to call `init` during deployment 85 | function test_deployProxyAndCall_init() public { 86 | proxy = deployProxyAndCall(address(logic), abi.encodePacked(Logic.init.selector)); 87 | 88 | assertEq(Logic(proxy).initializedCount(), 1); 89 | } 90 | 91 | /// call a nonexistent init function 92 | function test_deployProxyAndCall_fail_fallback() public { 93 | vm.expectRevert(); 94 | 95 | proxy = deployProxyAndCall(address(logic), abi.encodePacked("abcd")); 96 | } 97 | 98 | /// deploy and upgrade to an invalid address (EOA) 99 | function test_deployProxyAndCall_fail_NotAContract(bytes memory initCalldata) public { 100 | vm.expectRevert(NotAContract.selector); 101 | 102 | proxy = deployProxyAndCall(address(0xb0b), initCalldata); 103 | } 104 | 105 | /// deploy and upgrade to contract with an invalid uuid 106 | function test_deployProxyAndCall_fail_InvalidUUID(bytes memory initCalldata) public { 107 | address logic2 = address(new LogicInvalidUUID()); 108 | 109 | vm.expectRevert(InvalidUUID.selector); 110 | 111 | proxy = deployProxyAndCall(address(logic2), initCalldata); 112 | } 113 | 114 | /// deploy and upgrade to a contract that doesn't implement proxiableUUID 115 | /// this one reverts differently depending on proxy.. 116 | function test_deployProxyAndCall_fail_NonexistentUUID(bytes memory initCalldata) public { 117 | address logic2 = address(new LogicNonexistentUUID()); 118 | 119 | vm.expectRevert(); 120 | 121 | proxy = deployProxyAndCall(address(logic2), initCalldata); 122 | } 123 | 124 | /// note: making all of these testFail, because 125 | /// retrieving a custom error during contract deployment 126 | /// is too messy, since it doesn't work by default 127 | /// (returndatasize() is always 0). 128 | /// I had tried encoding it in the deployed code 129 | /// using the convention that if the first byte is 00 130 | /// then the rest would be the revert message 131 | /// and tested it out for the bytecode proxy (tests pass) 132 | 133 | /// call a reverting init function 134 | function testFail_deployProxyAndCall_fail_RevertOnInit() public { 135 | // vm.expectRevert(RevertOnInit.selector); 136 | 137 | proxy = deployProxyAndCall(address(logic), abi.encodePacked(Logic.initReverts.selector)); 138 | } 139 | 140 | /// call a reverting function during deployment 141 | /// make sure the error is returned 142 | function testFail_deployProxyAndCall_fail_initRevertsCustomMessage(bytes memory message) public { 143 | // vm.expectRevert(message); 144 | 145 | bytes memory initCalldata = abi.encodeWithSelector(Logic.initRevertsCustomMessage.selector, message); 146 | 147 | proxy = deployProxyAndCall(address(logic), initCalldata); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/UUPSUpgrade.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | import {MockUUPSUpgrade} from "./mocks/MockUUPSUpgrade.sol"; 7 | 8 | import "/ERC1967Proxy.sol"; 9 | 10 | // --------------------------------------------------------------------- 11 | // Mock Logic 12 | // --------------------------------------------------------------------- 13 | 14 | error RevertOnInit(); 15 | 16 | contract MockUUPSUpgradeV1 is MockUUPSUpgrade { 17 | uint256 public data = 0x1337; 18 | uint256 public initializedCount; 19 | 20 | uint256 public constant version = 1; 21 | 22 | function init() public { 23 | ++initializedCount; 24 | } 25 | 26 | function fn() public pure returns (uint256) { 27 | return 1337; 28 | } 29 | 30 | function setData(uint256 newData) public { 31 | data = newData; 32 | } 33 | } 34 | 35 | contract MockUUPSUpgradeV2 is MockUUPSUpgrade { 36 | address public data = address(0x42); 37 | 38 | uint256 public constant version = 2; 39 | 40 | function initReverts() public pure { 41 | revert RevertOnInit(); 42 | } 43 | 44 | function fn() public pure returns (uint256) { 45 | return 6969; 46 | } 47 | 48 | function fn2() public pure returns (uint256) { 49 | return 3141; 50 | } 51 | 52 | function initRevertsWithMessage(string memory message) public pure { 53 | require(false, message); 54 | } 55 | } 56 | 57 | contract LogicNonexistentUUID {} 58 | 59 | contract LogicInvalidUUID { 60 | bytes32 public proxiableUUID = 0x0000000000000000000000000000000000000000000000000000000000001234; 61 | } 62 | 63 | // --------------------------------------------------------------------- 64 | // UUPSUpgrade Tests 65 | // --------------------------------------------------------------------- 66 | 67 | contract TestUUPSUpgrade is Test { 68 | event Upgraded(address indexed implementation); 69 | 70 | MockUUPSUpgradeV1 logicV1; 71 | MockUUPSUpgradeV2 logicV2; 72 | address proxy; 73 | 74 | function deployProxyAndCall(address implementation, bytes memory initCalldata) internal virtual returns (address) { 75 | return address(new ERC1967Proxy(address(implementation), initCalldata)); 76 | } 77 | 78 | function setUp() public virtual { 79 | logicV1 = new MockUUPSUpgradeV1(); 80 | logicV2 = new MockUUPSUpgradeV2(); 81 | 82 | proxy = deployProxyAndCall(address(logicV1), ""); 83 | } 84 | 85 | /* ------------- setUp() ------------- */ 86 | 87 | function test_setUp() public { 88 | assertEq(logicV1.version(), 1); 89 | assertEq(logicV2.version(), 2); 90 | assertEq(MockUUPSUpgradeV1(proxy).version(), 1); 91 | 92 | assertEq(logicV1.fn(), 1337); 93 | assertEq(logicV2.fn(), 6969); 94 | assertEq(logicV2.fn2(), 3141); 95 | assertEq(MockUUPSUpgradeV1(proxy).fn(), 1337); 96 | 97 | assertEq(logicV1.data(), 0x1337); 98 | assertEq(logicV2.data(), address(0x42)); 99 | 100 | assertEq(MockUUPSUpgradeV1(proxy).data(), 0); 101 | assertEq(MockUUPSUpgradeV1(proxy).initializedCount(), 0); 102 | } 103 | 104 | /* ------------- upgradeToAndCall() ------------- */ 105 | 106 | /// expect implementation logic to change on upgrade 107 | function test_upgradeToAndCall_logic() public { 108 | assertEq(MockUUPSUpgradeV1(proxy).data(), 0); 109 | 110 | // proxy can call v1's setData 111 | MockUUPSUpgradeV1(proxy).setData(0x3333); 112 | 113 | assertEq(MockUUPSUpgradeV1(proxy).data(), 0x3333); // proxy's data now has changed 114 | assertEq(logicV1.data(), 0x1337); // implementation's data remains unchanged 115 | 116 | // -> upgrade to v2 117 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), ""); 118 | 119 | // test v2 functions 120 | assertEq(MockUUPSUpgradeV2(proxy).fn(), 6969); 121 | assertEq(MockUUPSUpgradeV2(proxy).fn2(), 3141); 122 | 123 | // make sure data remains unchanged (though returned as address now) 124 | assertEq(MockUUPSUpgradeV2(proxy).data(), address(0x3333)); 125 | 126 | // only available under v1 logic 127 | vm.expectRevert(); 128 | MockUUPSUpgradeV1(proxy).setData(0x456); 129 | 130 | // <- upgrade back to v1 131 | MockUUPSUpgradeV2(proxy).upgradeToAndCall(address(logicV1), ""); 132 | 133 | // v1's setData works again 134 | MockUUPSUpgradeV1(proxy).setData(0x6666); 135 | 136 | assertEq(MockUUPSUpgradeV1(proxy).data(), 0x6666); 137 | 138 | // only available under v2 logic 139 | vm.expectRevert(); 140 | MockUUPSUpgradeV2(proxy).fn2(); 141 | } 142 | 143 | /// expect implementation to be stored correctly 144 | function test_upgradeToAndCall_implementation() public { 145 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), ""); 146 | 147 | MockUUPSUpgrade(proxy).scrambleStorage(0, 100); 148 | 149 | assertEq(MockUUPSUpgradeV1(proxy).implementation(), address(logicV2)); 150 | assertEq(vm.load(proxy, ERC1967_PROXY_STORAGE_SLOT), bytes32(uint256(uint160(address(logicV2))))); 151 | } 152 | 153 | /// expect Upgraded(address) to be emitted 154 | function test_upgradeToAndCall_emit() public { 155 | vm.expectEmit(true, false, false, false, proxy); 156 | 157 | emit Upgraded(address(logicV2)); 158 | 159 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), ""); 160 | 161 | vm.expectEmit(true, false, false, false, proxy); 162 | 163 | emit Upgraded(address(logicV1)); 164 | 165 | MockUUPSUpgradeV2(proxy).upgradeToAndCall(address(logicV1), ""); 166 | } 167 | 168 | /// expect upgradeToAndCall to actually call the function 169 | function test_upgradeToAndCall_init() public { 170 | assertEq(MockUUPSUpgradeV1(proxy).initializedCount(), 0); 171 | 172 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV1), abi.encodePacked(MockUUPSUpgradeV1.init.selector)); 173 | 174 | assertEq(MockUUPSUpgradeV1(proxy).initializedCount(), 1); 175 | 176 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV1), abi.encodePacked(MockUUPSUpgradeV1.init.selector)); 177 | 178 | assertEq(MockUUPSUpgradeV1(proxy).initializedCount(), 2); 179 | } 180 | 181 | /// upgrade and call a nonexistent init function 182 | function test_upgradeToAndCall_fail_fallback() public { 183 | vm.expectRevert(); 184 | 185 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), "abcd"); 186 | } 187 | 188 | /// upgrade to v2 and call a reverting init function 189 | /// expect revert reason to bubble up 190 | function test_upgradeToAndCall_fail_RevertOnInit() public { 191 | vm.expectRevert(RevertOnInit.selector); 192 | 193 | MockUUPSUpgradeV1(proxy).upgradeToAndCall( 194 | address(logicV2), 195 | abi.encodePacked(MockUUPSUpgradeV2.initReverts.selector) 196 | ); 197 | } 198 | 199 | /// call a reverting function during upgrade 200 | /// make sure the error is returned 201 | function test_upgradeToAndCall_fail_initRevertsWithMessage(string memory message) public { 202 | vm.expectRevert(bytes(message)); 203 | 204 | bytes memory initCalldata = abi.encodeWithSelector(MockUUPSUpgradeV2.initRevertsWithMessage.selector, message); 205 | 206 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), initCalldata); 207 | } 208 | 209 | /// upgrade to an invalid address (EOA) 210 | function test_upgradeToAndCall_fail_NotAContract(bytes memory initCalldata) public { 211 | vm.expectRevert(NotAContract.selector); 212 | 213 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(0xb0b), initCalldata); 214 | } 215 | 216 | /// upgrade to contract with an invalid uuid 217 | function test_upgradeToAndCall_fail_InvalidUUID(bytes memory initCalldata) public { 218 | address logic = address(new LogicInvalidUUID()); 219 | 220 | vm.expectRevert(InvalidUUID.selector); 221 | 222 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(logic, initCalldata); 223 | } 224 | 225 | /// upgrade to a contract that doesn't implement proxiableUUID 226 | function test_upgradeToAndCall_fail_NonexistentUUID(bytes memory initCalldata) public { 227 | address logic = address(new LogicNonexistentUUID()); 228 | 229 | vm.expectRevert(); 230 | 231 | MockUUPSUpgradeV1(proxy).upgradeToAndCall(logic, initCalldata); 232 | } 233 | 234 | // /* ------------- gas ------------- */ 235 | 236 | // function testGas_deploy() public { 237 | // deployProxyAndCall(address(logicV1), ""); 238 | // } 239 | 240 | // function testGas_upgradeTo() public { 241 | // MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), ""); 242 | // } 243 | 244 | // function testGas_upgradeToAndCall() public { 245 | // MockUUPSUpgradeV1(proxy).upgradeToAndCall(address(logicV2), abi.encodePacked(MockUUPSUpgradeV2.fn.selector)); 246 | // } 247 | 248 | // function testGas_version() public view { 249 | // MockUUPSUpgradeV1(proxy).version(); 250 | // } 251 | } 252 | -------------------------------------------------------------------------------- /test/UUPSUpgradeWithImmutableArgs.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | import {ProxyTestDeployer} from "./utils/ProxyTestDeployer.sol"; 7 | 8 | import {LibERC1967ProxyWithImmutableArgs} from "/LibERC1967ProxyWithImmutableArgs.sol"; 9 | import {MockUUPSUpgrade} from "./mocks/MockUUPSUpgrade.sol"; 10 | 11 | // --------------------------------------------------------------------- 12 | // Mocks 13 | // --------------------------------------------------------------------- 14 | 15 | contract MockBase { 16 | uint256 immutable argOffset; 17 | uint256 immutable argLen; 18 | 19 | constructor(uint256 argOffset_, uint256 argLen_) { 20 | argOffset = argOffset_; 21 | argLen = argLen_; 22 | } 23 | } 24 | 25 | contract MockGetMsgData { 26 | fallback() external payable { 27 | assembly { 28 | calldatacopy(0, 0, calldatasize()) 29 | return(0, calldatasize()) 30 | } 31 | } 32 | } 33 | 34 | contract MockGetImmutableArgsLen { 35 | fallback() external payable { 36 | uint256 size = LibERC1967ProxyWithImmutableArgs.getImmutableArgsLen(); 37 | assembly { 38 | mstore(0, size) 39 | return(0, 0x20) 40 | } 41 | } 42 | } 43 | 44 | contract MockGetImmutableArgsOffset { 45 | fallback() external payable { 46 | uint256 offset = LibERC1967ProxyWithImmutableArgs.getImmutableArgsOffset(); 47 | assembly { 48 | mstore(0, offset) 49 | return(0, 0x20) 50 | } 51 | } 52 | } 53 | 54 | contract MockGetArgBytes is MockBase { 55 | constructor(uint256 argOffset, uint256 argLen) MockBase(argOffset, argLen) {} 56 | 57 | fallback() external payable { 58 | bytes memory arg = LibERC1967ProxyWithImmutableArgs.getArgBytes(argOffset, argLen); 59 | 60 | assembly { 61 | return(add(arg, 0x20), mload(arg)) 62 | } 63 | } 64 | } 65 | 66 | contract MockGetArgBytes32 is MockBase { 67 | constructor(uint256 argOffset, uint256 argLen) MockBase(argOffset, argLen) {} 68 | 69 | fallback() external payable { 70 | bytes32 arg = LibERC1967ProxyWithImmutableArgs.getArgBytes32(argOffset); 71 | 72 | assembly { 73 | mstore(0, arg) 74 | return(0, 0x20) 75 | } 76 | } 77 | } 78 | 79 | contract MockGetArgUint256 is MockBase { 80 | constructor(uint256 argOffset, uint256 argLen) MockBase(argOffset, argLen) {} 81 | 82 | fallback() external payable { 83 | uint256 arg = LibERC1967ProxyWithImmutableArgs.getArgUint256(argOffset); 84 | 85 | assembly { 86 | mstore(0, arg) 87 | return(0, 0x20) 88 | } 89 | } 90 | } 91 | 92 | contract MockGetArgUint128 is MockBase { 93 | constructor(uint128 argOffset, uint128 argLen) MockBase(argOffset, argLen) {} 94 | 95 | fallback() external payable { 96 | uint128 arg = LibERC1967ProxyWithImmutableArgs.getArgUint128(argOffset); 97 | 98 | assembly { 99 | mstore(0, arg) 100 | return(0, 0x20) 101 | } 102 | } 103 | } 104 | 105 | contract MockGetArgUint64 is MockBase { 106 | constructor(uint256 argOffset, uint256 argLen) MockBase(argOffset, argLen) {} 107 | 108 | fallback() external payable { 109 | uint64 arg = LibERC1967ProxyWithImmutableArgs.getArgUint64(argOffset); 110 | 111 | assembly { 112 | mstore(0, arg) 113 | return(0, 0x20) 114 | } 115 | } 116 | } 117 | 118 | contract MockGetArgUint40 is MockBase { 119 | constructor(uint256 argOffset, uint256 argLen) MockBase(argOffset, argLen) {} 120 | 121 | fallback() external payable { 122 | uint40 arg = LibERC1967ProxyWithImmutableArgs.getArgUint40(argOffset); 123 | 124 | assembly { 125 | mstore(0, arg) 126 | return(0, 0x20) 127 | } 128 | } 129 | } 130 | 131 | contract MockGetArgUint8 is MockBase { 132 | constructor(uint256 argOffset, uint256 argLen) MockBase(argOffset, argLen) {} 133 | 134 | fallback() external payable { 135 | uint8 arg = LibERC1967ProxyWithImmutableArgs.getArgUint8(argOffset); 136 | 137 | assembly { 138 | mstore(0, arg) 139 | return(0, 0x20) 140 | } 141 | } 142 | } 143 | 144 | // --------------------------------------------------------------------- 145 | // Immutable Args Tests 146 | // --------------------------------------------------------------------- 147 | 148 | contract TestImmutableArgs is Test { 149 | address logic; 150 | 151 | function setUp() public { 152 | logic = address(new MockUUPSUpgrade()); 153 | } 154 | 155 | /* ------------- helpers ------------- */ 156 | 157 | function boundParameterRange( 158 | uint256 immutableArgsLen, 159 | uint256 readArgOffset, 160 | uint256 readArgLen 161 | ) internal pure returns (uint16, uint16) { 162 | readArgOffset %= immutableArgsLen + 1; 163 | readArgLen %= immutableArgsLen + 1 - readArgOffset; 164 | 165 | return (uint16(readArgOffset), uint16(readArgLen)); 166 | } 167 | 168 | function expectReturnedArg( 169 | address proxy, 170 | bytes calldata randomCalldata, 171 | bytes calldata immutableArgs, 172 | uint256 readArgOffset, 173 | uint256 readArgLen 174 | ) internal { 175 | (, bytes memory returndata) = address(proxy).call(randomCalldata); 176 | 177 | bytes32 returnedArg = abi.decode(returndata, (bytes32)); 178 | 179 | bytes32 expectedArg; 180 | 181 | assembly { 182 | expectedArg := calldataload(add(immutableArgs.offset, readArgOffset)) 183 | expectedArg := shr(shl(3, sub(32, readArgLen)), expectedArg) 184 | } 185 | 186 | assertEq(returnedArg, expectedArg); 187 | } 188 | 189 | /* ------------- tests ------------- */ 190 | 191 | function test_getMsgData(bytes memory randomCalldata, bytes memory immutableArgs) public { 192 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 193 | 194 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetMsgData())); 195 | 196 | (, bytes memory returndata) = address(proxy).call(randomCalldata); 197 | 198 | assertEq(returndata, abi.encodePacked(randomCalldata, immutableArgs, uint16(immutableArgs.length))); 199 | } 200 | 201 | function test_getArgsLen(bytes memory randomCalldata, bytes memory immutableArgs) public { 202 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 203 | 204 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetImmutableArgsLen())); 205 | 206 | (, bytes memory returndata) = address(proxy).call(randomCalldata); 207 | 208 | uint256 len = abi.decode(returndata, (uint256)); 209 | 210 | assertEq(len, immutableArgs.length); 211 | } 212 | 213 | function test_getArgsOffset(bytes memory randomCalldata, bytes memory immutableArgs) public { 214 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 215 | 216 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetImmutableArgsOffset())); 217 | 218 | (, bytes memory returndata) = address(proxy).call(randomCalldata); 219 | 220 | uint256 offset = abi.decode(returndata, (uint256)); 221 | 222 | assertEq(offset, randomCalldata.length); 223 | } 224 | 225 | function test_getArgBytes( 226 | bytes calldata randomCalldata, 227 | bytes calldata immutableArgs, 228 | uint16 readArgOffset, 229 | uint16 readArgLen 230 | ) public { 231 | (readArgOffset, readArgLen) = boundParameterRange(immutableArgs.length, readArgOffset, readArgLen); 232 | 233 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 234 | 235 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgBytes(readArgOffset, readArgLen))); 236 | 237 | (, bytes memory returndata) = address(proxy).call(randomCalldata); 238 | 239 | assertEq(returndata, immutableArgs[readArgOffset:readArgOffset + readArgLen]); 240 | } 241 | 242 | function test_getArgBytes32( 243 | uint16 readArgOffset, 244 | bytes calldata randomCalldata, 245 | bytes calldata immutableArgs 246 | ) public { 247 | uint16 readArgLen = 32; 248 | 249 | (readArgOffset, ) = boundParameterRange(immutableArgs.length, readArgOffset, 0); 250 | 251 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 252 | 253 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgBytes32(readArgOffset, readArgLen))); 254 | 255 | expectReturnedArg(proxy, randomCalldata, immutableArgs, readArgOffset, readArgLen); 256 | } 257 | 258 | function test_getArgUint256( 259 | uint16 readArgOffset, 260 | bytes calldata randomCalldata, 261 | bytes calldata immutableArgs 262 | ) public { 263 | uint16 readArgLen = 32; 264 | 265 | (readArgOffset, ) = boundParameterRange(immutableArgs.length, readArgOffset, 0); 266 | 267 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 268 | 269 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgUint256(readArgOffset, readArgLen))); 270 | 271 | expectReturnedArg(proxy, randomCalldata, immutableArgs, readArgOffset, readArgLen); 272 | } 273 | 274 | function test_getArgUint128( 275 | uint16 readArgOffset, 276 | bytes calldata randomCalldata, 277 | bytes calldata immutableArgs 278 | ) public { 279 | uint16 readArgLen = 16; 280 | 281 | (readArgOffset, ) = boundParameterRange(immutableArgs.length, readArgOffset, 0); 282 | 283 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 284 | 285 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgUint128(readArgOffset, readArgLen))); 286 | 287 | expectReturnedArg(proxy, randomCalldata, immutableArgs, readArgOffset, readArgLen); 288 | } 289 | 290 | function test_getArgUint64( 291 | uint16 readArgOffset, 292 | bytes calldata randomCalldata, 293 | bytes calldata immutableArgs 294 | ) public { 295 | uint16 readArgLen = 8; 296 | 297 | (readArgOffset, ) = boundParameterRange(immutableArgs.length, readArgOffset, 0); 298 | 299 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 300 | 301 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgUint64(readArgOffset, readArgLen))); 302 | 303 | expectReturnedArg(proxy, randomCalldata, immutableArgs, readArgOffset, readArgLen); 304 | } 305 | 306 | function test_getArgUint40( 307 | uint16 readArgOffset, 308 | bytes calldata randomCalldata, 309 | bytes calldata immutableArgs 310 | ) public { 311 | uint16 readArgLen = 5; 312 | 313 | (readArgOffset, ) = boundParameterRange(immutableArgs.length, readArgOffset, 0); 314 | 315 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 316 | 317 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgUint40(readArgOffset, readArgLen))); 318 | 319 | expectReturnedArg(proxy, randomCalldata, immutableArgs, readArgOffset, readArgLen); 320 | } 321 | 322 | function test_getArgUint8( 323 | uint16 readArgOffset, 324 | bytes calldata randomCalldata, 325 | bytes calldata immutableArgs 326 | ) public { 327 | uint16 readArgLen = 1; 328 | 329 | (readArgOffset, ) = boundParameterRange(immutableArgs.length, readArgOffset, 0); 330 | 331 | address proxy = LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs(logic, "", immutableArgs); 332 | 333 | MockUUPSUpgrade(proxy).forceUpgrade(address(new MockGetArgUint8(readArgOffset, readArgLen))); 334 | 335 | expectReturnedArg(proxy, randomCalldata, immutableArgs, readArgOffset, readArgLen); 336 | } 337 | } 338 | 339 | // --------------------------------------------------------------------- 340 | // re-run all tests from "./UUPSUpgrade.t.sol" for proxy with immutable args 341 | // --------------------------------------------------------------------- 342 | 343 | import {TestUUPSUpgrade} from "./UUPSUpgrade.t.sol"; 344 | 345 | contract TestUUPSUpgradeWithImmutableArgs is TestUUPSUpgrade { 346 | function deployProxyAndCall(address implementation, bytes memory initCalldata) internal override returns (address) { 347 | return 348 | LibERC1967ProxyWithImmutableArgs.deployProxyWithImmutableArgs( 349 | implementation, 350 | initCalldata, 351 | abi.encode(keccak256("arg1"), keccak256("arg2"), keccak256("arg3")) 352 | ); 353 | } 354 | } 355 | 356 | // --------------------------------------------------------------------- 357 | // re-run all tests from "./ERC1967.t.sol" for proxy with immutable args 358 | // - these tests need to be run from a `Deployer` contract, 359 | // so that forge can catch the reverts on external calls 360 | // --------------------------------------------------------------------- 361 | 362 | import {TestERC1967} from "./ERC1967.t.sol"; 363 | 364 | contract TestERC1967WithImmutableArgs is TestERC1967 { 365 | ProxyTestDeployer deployer; 366 | 367 | function setUp() public override { 368 | deployer = new ProxyTestDeployer(); 369 | super.setUp(); 370 | } 371 | 372 | function deployProxyAndCall(address implementation, bytes memory initCalldata) internal override returns (address) { 373 | return 374 | deployer.deployProxyWithImmutableArgs( 375 | implementation, 376 | initCalldata, 377 | abi.encode(keccak256("arg1"), keccak256("arg2"), keccak256("arg3")) 378 | ); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /test/mocks/MockUUPSUpgrade.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC1967_PROXY_STORAGE_SLOT} from "/ERC1967Proxy.sol"; 5 | import {UUPSUpgrade} from "/UUPSUpgrade.sol"; 6 | 7 | contract MockUUPSUpgrade is UUPSUpgrade { 8 | function implementation() public view returns (address impl) { 9 | assembly { 10 | impl := sload(ERC1967_PROXY_STORAGE_SLOT) 11 | } 12 | } 13 | 14 | function forceUpgrade(address impl) public { 15 | assembly { 16 | sstore(ERC1967_PROXY_STORAGE_SLOT, impl) 17 | } 18 | } 19 | 20 | function scrambleStorage(uint256 offset, uint256 numSlots) public { 21 | bytes32 rand; 22 | for (uint256 slot; slot < numSlots; slot++) { 23 | rand = keccak256(abi.encodePacked(offset + slot)); 24 | 25 | assembly { 26 | sstore(add(slot, offset), rand) 27 | } 28 | } 29 | } 30 | 31 | function _authorizeUpgrade() internal virtual override {} 32 | } 33 | -------------------------------------------------------------------------------- /test/utils/ProxyTestDeployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {verifyIsProxiableContract, proxyRuntimeCode, proxyCreationCode} from "/utils/proxyCreationCode.sol"; 5 | import {LibERC1967ProxyWithImmutableArgs} from "/LibERC1967ProxyWithImmutableArgs.sol"; 6 | 7 | /// @notice wrapper contract for lib 8 | /// @notice forge testing doesn't work well with libs.. 9 | /// @notice should only be used for testing 10 | contract ProxyTestDeployer { 11 | function deployProxyWithImmutableArgs( 12 | address implementation, 13 | bytes memory initCalldata, 14 | bytes memory immutableArgs 15 | ) public returns (address) { 16 | verifyIsProxiableContract(implementation); 17 | 18 | bytes memory runtimeCode = proxyRuntimeCode(immutableArgs); 19 | bytes memory creationCode = proxyCreationCode(implementation, runtimeCode, initCalldata); 20 | 21 | return deployCodeBubbleUpRevertReason(creationCode); 22 | } 23 | 24 | /// @notice Should only be used for testing 25 | /// @notice Reads encoded revert reason in deployed bytecode 26 | /// @dev This requires `RETURN_REVERT_REASON_ON_FAILURE_TEST` 27 | /// @dev to be inserted in `initCallcode` to function properly 28 | function deployCodeBubbleUpRevertReason(bytes memory bytecode) internal returns (address payable addr) { 29 | assembly { 30 | addr := create(0, add(bytecode, 0x20), mload(bytecode)) 31 | 32 | // can't get any custom messages out of here... 33 | 34 | let ext_code_size := extcodesize(addr) 35 | 36 | if iszero(ext_code_size) { 37 | revert(0, 0) 38 | } 39 | 40 | // encode reason in bytecode just so I can run my tests... 41 | // by checking if first byte equals 00 (STOP) 42 | 43 | extcodecopy(addr, 0, 0, 1) 44 | 45 | if iszero(mload(0)) { 46 | let revert_size := sub(ext_code_size, 1) 47 | extcodecopy(addr, 0, 1, revert_size) 48 | revert(0, revert_size) 49 | } 50 | } 51 | } 52 | } 53 | --------------------------------------------------------------------------------