├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── huff └── UUPSProxy.huff ├── remappings.txt ├── script ├── Bench.sol └── Deploy.sol ├── src ├── Deployer.sol └── UpgradeableToken.sol └── test ├── UUPSProxy.t.sol └── mock ├── Implementation.sol └── UUPSProxy.sol /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Dotenv file 11 | .env 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | [submodule "lib/openzeppelin-contracts-upgradeable"] 8 | path = lib/openzeppelin-contracts-upgradeable 9 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimum Viable Proxy 2 | 3 | Arbitrum hackathon project comparing Open Zeppelin ERC1967 UUPS Proxy implementation with Huff. 4 | 5 | ## Example Deployment 6 | 7 | ``` 8 | 0xdEdbD1395c615e15AFDd14585271BE41C132787E 9 | ``` 10 | 11 | ## Gas Benchmarks Against ERC20Upgradeable 12 | 13 | | Huff Proxy | Open Zeppelin Proxy | function | 14 | | ---------- | ------------------- | ------------ | 15 | | 1617 | 1752 | name | 16 | | 1615 | 1750 | symbol | 17 | | 587 | 728 | decimals | 18 | | 736 | 877 | totalSupply | 19 | | 953 | 1091 | balanceOf | 20 | | 1225 | 1360 | allowance | 21 | | 3527 | 3659 | transfer | 22 | | 6266 | 6398 | transferFrom | 23 | | 3053 | 3185 | approve | 24 | | - | - | - | 25 | | 19579 | 20800 | TOTAL | 26 | 27 | ## Total Gas Diff: 28 | 29 | ``` 30 | Oz: 20,800 31 | Huff: 19,579 32 | ------------ 33 | Diff: 1,221 34 | ``` 35 | 36 | ## Runtime Bytecode Size Diff: 37 | 38 | ``` 39 | Oz: 699 40 | Huff: 62 41 | ------------ 42 | Diff: 637 43 | ``` 44 | 45 | ## Runtime Bytecode: 46 | 47 | Huff Proxy: 48 | 49 | ``` 50 | 363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735 51 | a920a3ca505d382bbc545af43d6000803e610039573d6000fd5b3d6000f3 52 | ``` 53 | 54 | OZ Proxy: 55 | 56 | ``` 57 | 60806040523661001357610011610017565b005b6100115b6100276100226100 58 | 67565b61009f565b565b606061004e8383604051806060016040528060278152 59 | 60200161025f602791396100c3565b9392505050565b6001600160a01b03163b 60 | 151590565b90565b600061009a7f360894a13ba1a3210667c828492db98dca3e 61 | 2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b 62 | 3660008037600080366000845af43d6000803e8080156100be573d6000f35b3d 63 | 6000fd5b6060600080856001600160a01b0316856040516100e0919061020f56 64 | 5b600060405180830381855af49150503d806000811461011b57604051915060 65 | 1f19603f3d011682016040523d82523d6000602084013e610120565b60609150 66 | 5b50915091506101318683838761013b565b9695505050505050565b60608315 67 | 6101af5782516000036101a8576001600160a01b0385163b6101a85760405162 68 | 461bcd60e51b815260206004820152601d60248201527f416464726573733a20 69 | 63616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b 70 | 60405180910390fd5b50816101b9565b6101b983836101c1565b949350505050 71 | 565b8151156101d15781518083602001fd5b8060405162461bcd60e51b815260 72 | 040161019f919061022b565b60005b8381101561020657818101518382015260 73 | 20016101ee565b50506000910152565b600082516102218184602087016101eb 74 | 565b9190910192915050565b602081526000825180602084015261024a816040 75 | 8501602087016101eb565b601f01601f1916919091016040019291505056fe41 76 | 6464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c20 77 | 6661696c6564a264697066735822122077a8973d907f53194c7809d7aaf9e3a0 78 | 89f162b31a6db09151edbb1cc1d9caa964736f6c63430008110033 79 | ``` 80 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /huff/UUPSProxy.huff: -------------------------------------------------------------------------------- 1 | // EIP1967 PROXY CONTRACT 2 | 3 | // ------------------------------------------------------------------------------------------------- 4 | // ABI 5 | 6 | #define function setImplementation(address) nonpayable returns () 7 | 8 | #define error Unauthorized() 9 | 10 | // ------------------------------------------------------------------------------------------------- 11 | // CONSTANTS 12 | 13 | // hard coding this. 14 | #define constant ADMIN_SLOT = 0x00 15 | 16 | // uint256(keccak256("eip1967.proxy.implementation")) - 1 17 | #define constant PROXY_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc 18 | 19 | #define constant WORD_SIZE = 0x20 20 | 21 | #define constant SELECTOR_LEN = 0x04 22 | 23 | #define constant ERROR_OFFSET = 0x1c 24 | 25 | #define macro CONSTRUCTOR() = takes (0) returns (0) { 26 | // STORE IMPLEMENTATION 27 | [WORD_SIZE] // [word] 28 | dup1 // [word, word] 29 | codesize // [codesize, word, word] 30 | sub // [impl_offset, word] 31 | returndatasize // [zero, impl_offset, word] 32 | codecopy // [] 33 | returndatasize // [zero] 34 | mload // [impl] 35 | [PROXY_SLOT] // [proxy_slot] 36 | sstore // [] 37 | } 38 | 39 | #define macro MAIN() = takes (0) returns (0) { 40 | // COPY CALLDATA TO MEMORY 41 | calldatasize // [calldatasize] 42 | returndatasize // [zero, calldatasize] 43 | returndatasize // [zero, zero, calldatasize] 44 | calldatacopy // [] 45 | 46 | // DELEGATECALL 47 | returndatasize // [retsize] 48 | returndatasize // [retoffset, retsize] 49 | calldatasize // [argsize, retoffset, retsize] 50 | returndatasize // [argoffset, argsize, retoffset, retsize] 51 | [PROXY_SLOT] // [proxy_slot, argoffset, argsize, retoffset, retsize] 52 | sload // [impl, argoffset, argsize, retoffset, retsize] 53 | gas // [gas, impl, argoffset, argsize, retoffset, retsize] 54 | delegatecall // [success] 55 | 56 | // COPY RETURNDATA TO MEMORY 57 | returndatasize // [retsize, success] 58 | 0x00 // [retoffset, retsize, success] 59 | dup1 // [memoffset, retoffset, retsize, success] 60 | returndatacopy // [success] 61 | 62 | // RETURN IF SUCCESS, ELSE BUBBLE UP ERROR 63 | call_success // [call_success, success] 64 | jumpi // [] 65 | 66 | // FAILED 67 | returndatasize // [retsize] 68 | 0x00 // [zero, retsize] 69 | revert // [] 70 | 71 | // SUCCESS 72 | call_success: 73 | returndatasize // [retsize] 74 | 0x00 // [zero, retsize] 75 | return // [] 76 | } 77 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | forge-std/=lib/forge-std/src/ 2 | oz/=lib/openzeppelin-contracts/contracts/ 3 | oz-up/=lib/openzeppelin-contracts-upgradeable/contracts/ -------------------------------------------------------------------------------- /script/Bench.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Script.sol"; 5 | import "oz/proxy/ERC1967/ERC1967Proxy.sol"; 6 | 7 | import "src/Deployer.sol"; 8 | import "src/UpgradeableToken.sol"; 9 | import "test/mock/UUPSProxy.sol"; 10 | 11 | using { compile } for Vm; 12 | using { create, create2, appendArg } for bytes; 13 | 14 | interface IERC20 { 15 | function name() external view returns (string memory); 16 | function symbol() external view returns (string memory); 17 | function decimals() external view returns (uint8); 18 | function totalSupply() external view returns (uint256); 19 | function balanceOf(address) external view returns (uint256); 20 | function allowance(address,address) external view returns (uint256); 21 | 22 | function transfer(address,uint256) external returns (bool); 23 | function transferFrom(address,address,uint256) external returns (bool); 24 | function approve(address,uint256) external returns (bool); 25 | } 26 | 27 | address constant alice = address(0x10); 28 | address constant bob = address(0x11); 29 | uint256 constant amount = 0; 30 | 31 | contract BenchScript is Script { 32 | UpgradeableToken huffProxy; 33 | UpgradeableToken ozProxy; 34 | address implementation; 35 | 36 | function run() public { 37 | // DEPLOY STUFF 38 | implementation = address(new UpgradeableToken()); 39 | 40 | huffProxy = UpgradeableToken( 41 | vm.compile("huff/UUPSProxy.huff").appendArg(implementation).create({value: 0}) 42 | ); 43 | 44 | ozProxy = UpgradeableToken(address(new ERC1967Proxy(implementation, new bytes(0)))); 45 | 46 | // INITIALIZE 47 | huffProxy.initialize(); 48 | ozProxy.initialize(); 49 | 50 | // WARM THE ADDRESS (requiring thing to shut the compiler up) 51 | (bool success, ) = implementation.staticcall(new bytes(0)); 52 | require(!success); 53 | 54 | // SET UP THE BENCH TESTS 55 | bytes[] memory staticcalls = new bytes[](6); 56 | staticcalls[0] = abi.encodeCall(IERC20.name, ()); 57 | staticcalls[1] = abi.encodeCall(IERC20.symbol, ()); 58 | staticcalls[2] = abi.encodeCall(IERC20.decimals, ()); 59 | staticcalls[3] = abi.encodeCall(IERC20.totalSupply, ()); 60 | staticcalls[4] = abi.encodeCall(IERC20.balanceOf, (alice)); 61 | staticcalls[5] = abi.encodeCall(IERC20.allowance, (alice, bob)); 62 | 63 | bytes[] memory calls = new bytes[](3); 64 | calls[0] = abi.encodeCall(IERC20.transfer, (bob, amount)); 65 | calls[1] = abi.encodeCall(IERC20.transferFrom, (alice, bob, amount)); 66 | calls[2] = abi.encodeCall(IERC20.approve, (bob, amount)); 67 | 68 | uint256[] memory huffgas = new uint256[](9); 69 | uint256[] memory ozgas = new uint256[](9); 70 | 71 | // RUN TESTS 72 | for (uint256 i; i < staticcalls.length; ++i) { 73 | (huffgas[i], ozgas[i]) = _makeStaticcall(huffProxy, ozProxy, staticcalls[i]); 74 | (huffgas[i], ozgas[i]) = _makeStaticcall(huffProxy, ozProxy, staticcalls[i]); 75 | } 76 | 77 | for (uint256 i; i < calls.length; ++i) { 78 | uint256 gasIndex = i + 6; 79 | (huffgas[gasIndex], ozgas[gasIndex]) = _makeCall(huffProxy, ozProxy, calls[i]); 80 | (huffgas[gasIndex], ozgas[gasIndex]) = _makeCall(huffProxy, ozProxy, calls[i]); 81 | } 82 | 83 | // PRINT BENCH 84 | for (uint256 i; i < huffgas.length; ++i) { 85 | console.log("---"); 86 | console.log(huffgas[i]); 87 | console.log(ozgas[i]); 88 | } 89 | 90 | console.log("--- BYTECODE SIZE ---"); 91 | console.log(address(huffProxy).code.length); 92 | console.log(address(ozProxy).code.length); 93 | 94 | 95 | console.log("--- BYTECODE ---"); 96 | console.logBytes(address(huffProxy).code); 97 | console.logBytes(address(ozProxy).code); 98 | 99 | (uint256 huffcost, uint256 ozcost) = _checkDeployCosts(); 100 | console.log("--- DEPLOYMENT COST ---"); 101 | console.log(huffcost); 102 | console.log(ozcost); 103 | } 104 | 105 | function _makeCall( 106 | UpgradeableToken huff, 107 | UpgradeableToken oz, 108 | bytes memory data 109 | ) internal returns (uint256 huffgas, uint256 ozgas) { 110 | assembly { 111 | let argoffset := add(data, 0x20) 112 | let argsize := mload(data) 113 | 114 | huffgas := gas() 115 | pop(call(gas(), huff, 0, argoffset, argsize, 0, 0)) 116 | huffgas := sub(huffgas, gas()) 117 | 118 | ozgas := gas() 119 | pop(call(gas(), oz, 0, argoffset, argsize, 0, 0)) 120 | ozgas := sub(ozgas, gas()) 121 | } 122 | } 123 | 124 | function _makeStaticcall( 125 | UpgradeableToken huff, 126 | UpgradeableToken oz, 127 | bytes memory data 128 | ) internal returns (uint256 huffgas, uint256 ozgas) { 129 | assembly { 130 | let argoffset := add(data, 0x20) 131 | let argsize := mload(data) 132 | 133 | huffgas := gas() 134 | pop(staticcall(gas(), huff, argoffset, argsize, 0, 0)) 135 | huffgas := sub(huffgas, gas()) 136 | 137 | ozgas := gas() 138 | pop(call(gas(), oz, 0, argoffset, argsize, 0, 0)) 139 | ozgas := sub(ozgas, gas()) 140 | } 141 | } 142 | 143 | function _checkDeployCosts() internal returns (uint256 huffgas, uint256 ozgas) { 144 | bytes memory huffcode = compiled.appendArg(implementation); 145 | 146 | assembly { 147 | let huffoffset := add(huffcode, 0x20) 148 | let huffsize := mload(huffcode) 149 | huffgas := gas() 150 | pop(create2(0, huffoffset, huffsize, 0)) 151 | huffgas := sub(huffgas, gas()) 152 | } 153 | 154 | ozgas = gasleft(); 155 | new ERC1967Proxy(implementation, new bytes(0)); 156 | unchecked { ozgas -= gasleft(); } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /script/Deploy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Script.sol"; 5 | import "src/Deployer.sol"; 6 | using { compile } for Vm; 7 | using { create, create2, appendArg } for bytes; 8 | 9 | contract DeployScript is Script { 10 | function run() public { 11 | address implementation = address(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1); 12 | vm.startBroadcast(vm.envUint("PRIVATE_KEY")); 13 | vm.compile("huff/UUPSProxy.huff").appendArg(implementation).create({value: 0}); 14 | vm.stopBroadcast(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Deployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import {Vm} from "forge-std/Vm.sol"; 5 | 6 | using { appendArg, create } for bytes; 7 | 8 | bytes constant compiled = hex"60208038033d393d517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc55603e8060343d393df3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e610039573d6000fd5b3d6000f3"; 9 | 10 | function useMinimumViableProxy(address implementation) returns (address) { 11 | return compiled.appendArg(implementation).create({value: 0}); 12 | } 13 | 14 | function compile(Vm vm, string memory path) returns (bytes memory) { 15 | string[] memory cmd = new string[](3); 16 | cmd[0] = "huffc"; 17 | cmd[1] = "--bytecode"; 18 | cmd[2] = path; 19 | return vm.ffi(cmd); 20 | } 21 | 22 | error DeploymentFailure(bytes bytecode); 23 | 24 | function create(bytes memory bytecode, uint256 value) returns (address deployedAddress) { 25 | assembly { 26 | deployedAddress := create(value, add(bytecode, 0x20), mload(bytecode)) 27 | } 28 | 29 | if (deployedAddress == address(0)) revert DeploymentFailure(bytecode); 30 | } 31 | 32 | function create2( 33 | bytes memory bytecode, 34 | uint256 value, 35 | bytes32 salt 36 | ) returns (address deployedAddress) { 37 | assembly { 38 | deployedAddress := create2(value, add(bytecode, 0x20), mload(bytecode), salt) 39 | } 40 | 41 | if (deployedAddress == address(0)) revert DeploymentFailure(bytecode); 42 | } 43 | 44 | function appendArg(bytes memory bytecode, address arg) pure returns (bytes memory) { 45 | return bytes.concat(bytecode, abi.encode(arg)); 46 | } 47 | -------------------------------------------------------------------------------- /src/UpgradeableToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "oz-up/token/ERC20/ERC20Upgradeable.sol"; 5 | import "oz-up/access/OwnableUpgradeable.sol"; 6 | import "oz-up/proxy/utils/Initializable.sol"; 7 | import "oz-up/proxy/utils/UUPSUpgradeable.sol"; 8 | 9 | contract UpgradeableToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable { 10 | /// @custom:oz-upgrades-unsafe-allow constructor 11 | constructor() { 12 | _disableInitializers(); 13 | } 14 | 15 | function initialize() initializer public { 16 | __ERC20_init("MyToken", "MTK"); 17 | __Ownable_init(); 18 | __UUPSUpgradeable_init(); 19 | } 20 | 21 | function _authorizeUpgrade(address newImplementation) 22 | internal 23 | onlyOwner 24 | override 25 | {} 26 | } 27 | -------------------------------------------------------------------------------- /test/UUPSProxy.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "forge-std/Test.sol"; 5 | import "src/Deployer.sol"; 6 | import "test/mock/Implementation.sol"; 7 | import "test/mock/UUPSProxy.sol"; 8 | 9 | using { compile } for Vm; 10 | using { create, create2, appendArg } for bytes; 11 | 12 | bytes32 constant PROXY_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 13 | 14 | contract CounterTest is Test { 15 | address impl; 16 | address proxy; 17 | 18 | function setUp() public { 19 | impl = address(new Implementation()); 20 | proxy = vm.compile("huff/UUPSPRoxy.huff") 21 | .appendArg(impl) 22 | .create({value: 0}); 23 | } 24 | 25 | function testSlot() public { 26 | assertEq(vm.load(proxy, PROXY_SLOT), bytes32(uint256(uint160(impl)))); 27 | } 28 | 29 | function testDelegatecall() public { 30 | uint256 beforeSet = Implementation(proxy).value(); 31 | 32 | Implementation(proxy).setValue(1); 33 | 34 | uint256 afterSet = Implementation(proxy).value(); 35 | 36 | assertEq(beforeSet, 0); 37 | assertEq(afterSet, 1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/mock/Implementation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | contract Implementation { 5 | uint256 internal lol; 6 | uint256 public value; 7 | 8 | function setValue(uint256 newValue) external { 9 | value = newValue; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/mock/UUPSProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import "oz/proxy/ERC1967/ERC1967Proxy.sol"; 5 | import "oz/access/Ownable.sol"; 6 | 7 | contract UUPSProxy is ERC1967Proxy, Ownable { 8 | constructor(address implementation) ERC1967Proxy(implementation, new bytes(0)) {} 9 | 10 | function setImplementation(address newImplementation) external { 11 | assembly { 12 | // compiler was yelling at me about the internal func, so we do it in asm 13 | sstore(_IMPLEMENTATION_SLOT, newImplementation) 14 | } 15 | } 16 | } 17 | --------------------------------------------------------------------------------