diff --git a/EIPS/eip-7229.md b/EIPS/eip-7229.md new file mode 100644 index 0000000000000..8c80b4416fddd --- /dev/null +++ b/EIPS/eip-7229.md @@ -0,0 +1,249 @@ +--- +eip: 7229 +title: Minimal Upgradable Proxy Contract +description: A lightweight contract upgrade pattern designed to save gas costs while providing the ability to upgrade contracts. +author: xiaobaiskill (@xiaobaiskill) +discussions-to: https://ethereum-magicians.org/t/eip-xxxx-minimal-upgradable-proxy/14754 +status: Draft +type: Standards Track +category: ERC +created: 2023-06-24 +--- + +## Abstract + +This proposal introduces the Minimal Upgradable Contract, a lightweight alternative to the existing upgradable contract implementation provided by OpenZeppelin. The Minimal Upgradable Contract aims to significantly reduce gas consumption during deployment while maintaining upgradability features. + +## Motivation + +Current upgradable contract solutions, such as OpenZeppelin's [ERC-1967](./eip-1967.md) implementation, often incur high gas costs during deployment. The goal of this proposal is to present an alternative approach that offers a significant reduction in gas consumption while maintaining the ability to upgrade contract logic. + +## Specification + +The exact bytecode of the standard minimal upgradable contract is this: +`7fxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx73yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3`; In this bytecode, the 1st to 32nd byte (inclusive) needs to be replaced with a 32-byte slot, and the 34th to 53rd byte (inclusive) needs to be replaced with a 20-byte address. +Please note that the placeholders `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` represent the 32-byte slot and `yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` represents the 20-byte address. + + +## Rationale + +The Minimal Upgradeable Contract proposal introduces a novel approach to minimize gas consumption while preserving the upgradability feature of smart contracts. By predefining the slot that stores the logic contract during the contract creation phase, we can optimize gas efficiency during contract execution by directly accessing and performing delegate calls to the logic contract stored in the designated slot. + +The rationale behind this approach is to eliminate the need for additional storage operations or costly lookups, which are commonly required in traditional upgradeable contract implementations. By predefining the slot and accessing the logic contract directly, we achieve significant gas savings and improve overall contract performance. + +Key considerations for adopting this rationale include: + +1、 Gas Optimization: By eliminating redundant storage operations and lookup procedures, we reduce gas consumption during contract execution. This optimization ensures that the contract remains cost-effective, especially when performing frequent contract calls or upgrades. + +2、 Deterministic Contract Logic: By fixing the slot for storing the logic contract during contract creation, we establish a deterministic relationship between the contract and its logic. This deterministic mapping enables seamless contract upgrades without requiring additional storage or lookup mechanisms. + +3、 Contract Compatibility: The rationale aligns with existing Ethereum Virtual Machine (EVM) environments and does not introduce any compatibility issues or modifications to the underlying infrastructure. It can be easily integrated into the current Ethereum ecosystem, ensuring seamless adoption by developers and users. + +4、 Developer-Friendly Implementation: The rationale simplifies the implementation process for upgradeable contracts. By providing a straightforward mechanism to access and delegate calls to the logic contract, developers can focus on contract logic and functionality rather than complex storage management. + +By adopting this rationale, we aim to strike a balance between gas efficiency and contract upgradability. It allows developers to deploy upgradeable contracts with minimal gas consumption, making them more accessible and economically viable for a wide range of applications. + + +### Gas Efficiency + +Compared to existing upgradable contract solutions, the Minimal Upgradable Contract demonstrates a significant reduction in gas consumption during deployment. While OpenZeppelin's [ERC-1967](./eip-1967.md) implementation may consumes nearly several hundred thousand gas for deployment, the Minimal Upgradable Contract can be deployed with just a few tens of thousands of gas, resulting in substantial cost savings. + +- [Transaction deploying the Minimal Upgradable Contract (32bytes slot)](../assets/eip-7229/img/minimal-upgradable-proxy-32slot.png) +- [Transaction deploying the Minimal Upgradable Contract (1bytes slot)](../assets/eip-7229/img/minimal-upgradable-proxy-1slot.png) +- [Transaction deploying using OpenZeppelin's ERC-1967](../assets/eip-7229/img/openzepplin-ERC-1967.png) + +### Implementation + +A reference implementation of the Minimal Upgradable Contract, including the proxy contract and an example implementation contract, will be provided as open-source code. This implementation will serve as a starting point for developers to adopt and customize the Minimal Upgradable Contract in their projects. + +#### Example implementations + +- [deploy proxy contract when deploying logic contract (32bytes slot)](../assets/eip-7229/contracts/mock/mock32/Example.sol) +- [deploy proxy contract (32bytes slot)](../assets/eip-7229/contracts/mock/mock32/DeployContract.sol) + +- [deploy proxy contract when deploying logic contract (1bytes slot)](../assets/eip-7229/contracts/mock/mock1/Example.sol) +- [deploy proxy contract (1bytes slot)](../assets/eip-7229/contracts/mock/mock1/DeployContract.sol) + +#### Standard Proxy + +The disassembly of the standard deployed proxy contract code + +``` +# store logic address to slot of proxy contract +PUSH32 [slot] +PUSH20 [logicAddress slot] +DUP2 [slot logicAddress slot] +SSTORE [slot] => storage(slot => logicAddress) + +# return deployedCode +PUSH1 0x9 [0x9 slot] +PUSH1 0x4c [0x4c 0x9 slot] +PUSH0 [00 0x4c 0x9 slot] +CODECOPY [slot] ==> memory(0x00~0x8: 0x4c~0x54(deployedCode1stPart)) +PUSH1 0x9 [0x9 slot] +MSTORE [] ==> memory(0x9~0x28: slot(deployedCode2ndPart)) +PUSH1 0x10 [0x10] +PUSH1 0x55 [0x55 0x10] +PUSH1 0x29 [0x29 0x55 0x10] +CODECOPY [] ==> memory(0x29~0x38: 0x55~0x64(deployedCode3rdPart)) +PUSH1 0x39 [0x39] +PUSH0 [00 0x39] +RETURN + +# proxy contract (deployedcode) +CALLDATASIZE [calldatasize] +PUSH0 [00 calldatasize] +PUSH0 [00 00 calldatasize] +CALLDATACOPY [] ==> memory(00~(calldatasize-1) => codedata) +PUSH0 [00] +PUSH0 [00 00] +CALLDATASIZE [calldatasize 00 00] +PUSH0 [00 calldatasize 00 00] +PUSH32 [slot 00 calldatasize 00 00] +SLOAD [logicAddress 00 calldatasize 00 00] +GAS [gas logicAddress 00 calldatasize 00 00] +DELEGATECALL [result] +RETURNDATASIZE [returnDataSize result] +PUSH0 [00 returnDataSize result] +PUSH0 [00 00 returnDataSize result] +RETURNDATACOPY [result] => memory(00~(RETURNDATASIZE - 1) => RETURNDATA) +RETURNDATASIZE [returnDataSize result] +PUSH0 [00 returnDataSize result] +DUP3 [result 00 returnDataSize result] +PUSH1 0x37 [0x37 result 00 returnDataSize result] +JUMPI [00 returnDataSize result] +REVERT [result] +JUMPDEST [00 returnDataSize result] +RETURN [result] +``` + +NOTE: To push a zero value onto the stack without abusing the `RETURNDATASIZE` opcode, the above code utilizes [EIP-3855](./eip-3855.md). It achieves this by using the `PUSH0` instruction to push the zero value. + +#### Storage slot of 1 byte optimization + +To further optimize the minimal upgradeable proxy by controlling the slot value for the logic address within 1 ~ 255(inclusive), you can use the following opcode to reduce gas consumption: + +``` +# store logic address to slot of proxy contract +PUSH1 [slot] +PUSH20 [logicAddress slot] +DUP2 [slot logicAddress slot] +SSTORE [slot] => storage(slot => logicAddress) + +# return deployedCode +PUSH1 0x9 [0x9 slot] +PUSH1 0x30 [0x30 0x9 slot] +PUSH0 [00 0x30 0x9 slot] +CODECOPY [slot] ==> memory(0x00~0x8: 0x30~0x38(deployedCode1stPart)) +PUSH1 0xf8 [0xf8 slot] +SHL [slotAfterShl] +PUSH1 0x9 [0x9 slotAfterShl] +MSTORE [] ==> memory(0x9: slotAfterShl(deployedCode2ndPart)) +PUSH1 0x10 [0x10] +PUSH1 0x39 [0x39 0x10] +PUSH1 0xa [0xa 0x39 0x10] +CODECOPY [] ==> memory(0xa~0x19: 0x39~0x48(deployedCode3rdPart)) +PUSH1 0x1a [0x1a] +PUSH0 [00 0x1a] +RETURN + +# proxy contract (deployedcode) +CALLDATASIZE [calldatasize] +PUSH0 [00 calldatasize] +PUSH0 [00 00 calldatasize] +CALLDATACOPY [] ==> memory(00~(calldatasize-1) => codedata) +PUSH0 [00] +PUSH0 [00 00] +CALLDATASIZE [calldatasize 00 00] +PUSH0 [00 calldatasize 00 00] +PUSH1 [slot 00 calldatasize 00 00] +SLOAD [logicAddress 00 calldatasize 00 00] +GAS [gas logicAddress 00 calldatasize 00 00] +DELEGATECALL [result] +RETURNDATASIZE [returnDataSize result] +PUSH0 [00 returnDataSize result] +PUSH0 [00 00 returnDataSize result] +RETURNDATACOPY [result] => memory(00~(RETURNDATASIZE - 1) => RETURNDATA) +RETURNDATASIZE [returnDataSize result] +PUSH0 [00 returnDataSize result] +DUP3 [result 00 returnDataSize result] +PUSH1 0x18 [0x18 result 00 returnDataSize result] +JUMPI [00 returnDataSize result] +REVERT [result] +JUMPDEST [00 returnDataSize result] +RETURN [result] +``` + +The bytecode generated by the above opcodes is as follows `60xx73yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3`, replace `xx` to a slot of 1byte and replace `yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` to a address of 20bytes before deploying contract + + +#### Optimization for Logic Address Storage in Slot 0 + +To further optimize the minimal upgradeable proxy by controlling the slot value for the logic address to 0, you can use the following opcode to reduce gas consumption: + +``` +# store logic address to slot of proxy contract +PUSH20 [logicAddress] +PUSH0 [00 logicAddress] +SSTORE [] => storage(00 => logicAddress) + +# return deployedCode +PUSH1 0x9 [0x9] +PUSH1 0x28 [0x28 0x9] +PUSH0 [00 0x28 0x9] +CODECOPY [] ==> memory(0x00~0x8: 0x28~0x30(deployedCode1stPart)) +PUSH1 0x10 [0x10] +PUSH1 0x31 [0x31 0x10] +PUSH1 0x9 [0x9 0x31 0x10] +CODECOPY [] ==> memory(0x9~0x19: 0x31~0x41(deployedCode2ndPart)) +PUSH1 0x19 [0x19] +PUSH0 [00 0x19] +RETURN + +# proxy contract (deployedcode) +CALLDATASIZE [calldatasize] +PUSH0 [00 calldatasize] +PUSH0 [00 00 calldatasize] +CALLDATACOPY [] ==> memory(00~(calldatasize-1) => codedata) +PUSH0 [00] +PUSH0 [00 00] +CALLDATASIZE [calldatasize 00 00] +PUSH0 [00 calldatasize 00 00] +PUSH0 [00 00 calldatasize 00 00] +SLOAD [logicAddress 00 calldatasize 00 00] +GAS [gas logicAddress 00 calldatasize 00 00] +DELEGATECALL [result] +RETURNDATASIZE [returnDataSize result] +PUSH0 [00 returnDataSize result] +PUSH0 [00 00 returnDataSize result] +RETURNDATACOPY [result] => memory(00~(RETURNDATASIZE - 1) => RETURNDATA) +RETURNDATASIZE [returnDataSize result] +PUSH0 [00 returnDataSize result] +DUP3 [result 00 returnDataSize result] +PUSH1 0x17 [0x17 result 00 returnDataSize result] +JUMPI [00 returnDataSize result] +REVERT [result] +JUMPDEST [00 returnDataSize result] +RETURN [result] +``` + +The bytecode generated by the above opcodes is as follows `73yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy5f55600960285f396010603160093960195ff3365f5f375f5f365f5f545af43d5f5f3e3d5f82601757fd5bf3`, replace `yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy` to a address of 20bytes before deploying contract + + +## Test Cases + +You can use the following commands: + +``` +cd ../assets/eip-7229 +npm install +npx hardhat test +``` + +## Security Considerations + +None. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-7229/.gitignore b/assets/eip-7229/.gitignore new file mode 100644 index 0000000000000..cb9923aae4a52 --- /dev/null +++ b/assets/eip-7229/.gitignore @@ -0,0 +1,5 @@ +artifacts +cache +node_modules +package-lock.json +typechain \ No newline at end of file diff --git a/assets/eip-7229/contracts/mock/PayableTokenV1.sol b/assets/eip-7229/contracts/mock/PayableTokenV1.sol new file mode 100644 index 0000000000000..6b7d62ef52411 --- /dev/null +++ b/assets/eip-7229/contracts/mock/PayableTokenV1.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract PayableTokenV1 { + address private implementation; + address public owner; + uint256 public number; + + event Upgraded(address indexed implementation); + + receive() external payable { + number += 1; + } + + fallback() external payable { + number += 2; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external payable { + require(msg.value >= 1 ether, "Insufficient amount"); + number = _number; + } +} diff --git a/assets/eip-7229/contracts/mock/PayableTokenV2.sol b/assets/eip-7229/contracts/mock/PayableTokenV2.sol new file mode 100644 index 0000000000000..3a2bae99666ae --- /dev/null +++ b/assets/eip-7229/contracts/mock/PayableTokenV2.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract PayableTokenV2 { + address private implementation; + address public owner; + uint256 public number; + + event Upgraded(address indexed implementation); + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external payable { + require(msg.value >= 1 ether, "Insufficient amount"); + number = _number; + } +} diff --git a/assets/eip-7229/contracts/mock/SimV1.sol b/assets/eip-7229/contracts/mock/SimV1.sol new file mode 100644 index 0000000000000..65b0c3134159f --- /dev/null +++ b/assets/eip-7229/contracts/mock/SimV1.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract SimV1 { + address public owner; + uint256 public number; + address private implementation; + + event Upgraded(address indexed implementation); + + constructor(uint256 _number) { + number = _number; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external OnlyOwner { + number = _number; + } +} diff --git a/assets/eip-7229/contracts/mock/SimV2.sol b/assets/eip-7229/contracts/mock/SimV2.sol new file mode 100644 index 0000000000000..c8a3b2a96f7f7 --- /dev/null +++ b/assets/eip-7229/contracts/mock/SimV2.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract SimV2 { + address public owner; + uint256 public number; + address private implementation; + uint96 public upgradeTime; + + event Upgraded(address indexed implementation); + + constructor(uint256 _number) { + number = _number; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + upgradeTime = uint96(block.timestamp); + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external OnlyOwner { + number = _number; + } + + function addNumber(uint256 _num) external { + number += _num; + } +} diff --git a/assets/eip-7229/contracts/mock/mock0/Example.sol b/assets/eip-7229/contracts/mock/mock0/Example.sol new file mode 100644 index 0000000000000..5fdfd4aef89e6 --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock0/Example.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "contracts/utils/Proxy0.sol"; + +contract Example0V1 is Proxy0 { + uint256 public number; + + constructor() Proxy0(true) {} + + function upgrade(address _newImplementation) external { + _upgrade(_newImplementation); + } + + function setNumber(uint256 _number) external { + number = _number; + } +} + +contract Example0V2 is Proxy0 { + uint256 public number; + + constructor() Proxy0(false) {} + + function upgrade(address _newImplementation) external { + _upgrade(_newImplementation); + } + + function setNumber(uint256 _number) external { + number = _number; + } + + function addNumber(uint256 _number) external { + number += _number; + } +} diff --git a/assets/eip-7229/contracts/mock/mock0/PayableToken0V1.sol b/assets/eip-7229/contracts/mock/mock0/PayableToken0V1.sol new file mode 100644 index 0000000000000..4d6660c45b109 --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock0/PayableToken0V1.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract PayableToken0V1 { + address private implementation; + address public owner; + uint256 public number; + + event Upgraded(address indexed implementation); + + receive() external payable { + number += 1; + } + + fallback() external payable { + number += 2; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external payable { + require(msg.value >= 1 ether, "Insufficient amount"); + number = _number; + } +} diff --git a/assets/eip-7229/contracts/mock/mock0/PayableToken0V2.sol b/assets/eip-7229/contracts/mock/mock0/PayableToken0V2.sol new file mode 100644 index 0000000000000..1d82c801f87dc --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock0/PayableToken0V2.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract PayableToken0V2 { + address private implementation; + address public owner; + uint256 public number; + + event Upgraded(address indexed implementation); + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external payable { + require(msg.value >= 1 ether, "Insufficient amount"); + number = _number; + } +} diff --git a/assets/eip-7229/contracts/mock/mock0/Sim0V1.sol b/assets/eip-7229/contracts/mock/mock0/Sim0V1.sol new file mode 100644 index 0000000000000..19b71146086f1 --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock0/Sim0V1.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract Sim0V1 { + address private implementation; + address public owner; + uint256 public number; + + event Upgraded(address indexed implementation); + + constructor(uint256 _number) { + number = _number; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external OnlyOwner { + number = _number; + } +} diff --git a/assets/eip-7229/contracts/mock/mock0/Sim0V2.sol b/assets/eip-7229/contracts/mock/mock0/Sim0V2.sol new file mode 100644 index 0000000000000..d80c69d38f864 --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock0/Sim0V2.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract Sim0V2 { + address private implementation; + address public owner; + uint256 public number; + uint96 public upgradeTime; + + event Upgraded(address indexed implementation); + + constructor(uint256 _number) { + number = _number; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (bytes32 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + upgradeTime = uint96(block.timestamp); + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external OnlyOwner { + number = _number; + } + + function addNumber(uint256 _num) external { + number += _num; + } +} diff --git a/assets/eip-7229/contracts/mock/mock1/DeployContract.sol b/assets/eip-7229/contracts/mock/mock1/DeployContract.sol new file mode 100644 index 0000000000000..22bf8d2261d5c --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock1/DeployContract.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract Test1 { + address public owner; + uint256 public number; + address private implementation; + + event Upgraded(address indexed implementation); + + constructor(uint256 _number) { + number = _number; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (uint8 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external OnlyOwner { + number = _number; + } +} + +contract DeployContract1 { + function createContract(Test1 _logic) external returns (address proxy) { + bytes memory code = abi.encodePacked( + hex"60", + _logic.getImplementSlot(), + hex"73", + _logic, + hex"8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3" + ); + assembly { + proxy := create2(0, add(code, 0x20), mload(code), 0x0) + if iszero(extcodesize(proxy)) { + revert(0, 0) + } + } + } + + function precomputeContract(Test1 _logic) external view returns (address) { + bytes memory code = abi.encodePacked( + hex"60", + _logic.getImplementSlot(), + hex"73", + _logic, + hex"8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3" + ); + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + uint256(0), + keccak256(code) + ) + ) + ) + ) + ); + } +} diff --git a/assets/eip-7229/contracts/mock/mock1/Example.sol b/assets/eip-7229/contracts/mock/mock1/Example.sol new file mode 100644 index 0000000000000..e42d4df5baf2c --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock1/Example.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "contracts/utils/Proxy1.sol"; + +contract Example1V1 is Proxy1 { + uint256 public number; + + constructor() Proxy1(true) {} + + function upgrade(address _newImplementation) external { + _upgrade(_newImplementation); + } + + function setNumber(uint256 _number) external { + number = _number; + } +} + +contract Example1V2 is Proxy1 { + uint256 public number; + + constructor() Proxy1(false) {} + + function upgrade(address _newImplementation) external { + _upgrade(_newImplementation); + } + + function setNumber(uint256 _number) external { + number = _number; + } + + function addNumber(uint256 _number) external { + number += _number; + } +} diff --git a/assets/eip-7229/contracts/mock/mock32/DeployContract.sol b/assets/eip-7229/contracts/mock/mock32/DeployContract.sol new file mode 100644 index 0000000000000..5bbbd13769749 --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock32/DeployContract.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract Test32 { + address public owner; + uint256 public number; + address private implementation; + + event Upgraded(address indexed implementation); + + constructor(uint256 _number) { + number = _number; + } + + modifier OnlyOwner() { + require(owner == msg.sender, "only owner"); + _; + } + + function init() external { + owner = msg.sender; + } + + function getImplementSlot() external pure returns (uint256 slot) { + assembly { + slot := implementation.slot + } + } + + function upgrade(address _newImplementation) external OnlyOwner { + implementation = _newImplementation; + emit Upgraded(_newImplementation); + } + + function setNumber(uint256 _number) external OnlyOwner { + number = _number; + } +} + +contract DeployContract32 { + function createContract(Test32 _logic) external returns (address proxy) { + bytes memory code = abi.encodePacked( + hex"7f", + _logic.getImplementSlot(), + hex"73", + _logic, + hex"81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3" + ); + assembly { + proxy := create2(0, add(code, 0x20), mload(code), 0x0) + if iszero(extcodesize(proxy)) { + revert(0, 0) + } + } + } + + function precomputeContract(Test32 _logic) external view returns (address) { + bytes memory code = abi.encodePacked( + hex"7f", + _logic.getImplementSlot(), + hex"73", + _logic, + hex"81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3" + ); + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + uint256(0), + keccak256(code) + ) + ) + ) + ) + ); + } +} diff --git a/assets/eip-7229/contracts/mock/mock32/Example.sol b/assets/eip-7229/contracts/mock/mock32/Example.sol new file mode 100644 index 0000000000000..cfd60a9552948 --- /dev/null +++ b/assets/eip-7229/contracts/mock/mock32/Example.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "contracts/utils/Proxy32.sol"; + +contract Example32V1 is Proxy32 { + uint256 public number; + + constructor() Proxy32(true) {} + + function upgrade(address _newImplementation) external { + _upgrade(_newImplementation); + } + + function setNumber(uint256 _number) external { + number = _number; + } +} + +contract Example32V2 is Proxy32 { + uint256 public number; + + constructor() Proxy32(false) {} + + function upgrade(address _newImplementation) external { + _upgrade(_newImplementation); + } + + function setNumber(uint256 _number) external { + number = _number; + } + + function addNumber(uint256 _number) external { + number += _number; + } +} diff --git a/assets/eip-7229/contracts/utils/Proxy0.sol b/assets/eip-7229/contracts/utils/Proxy0.sol new file mode 100644 index 0000000000000..d94ddb96c80c6 --- /dev/null +++ b/assets/eip-7229/contracts/utils/Proxy0.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +abstract contract Proxy0 { + address private implementation; + + event Upgraded(address indexed implementation); + + constructor(bool _deployProxy) { + if (_deployProxy) { + uint256 slot; + assembly { + slot := implementation.slot + } + require(slot == 0, "implementation.slot must be zero"); + // deploy proxy contract by logic contract + bytes memory code = abi.encodePacked( + hex"73", + address(this), + hex"5f55600960285f396010603160093960195ff3365f5f375f5f365f5f545af43d5f5f3e3d5f82601757fd5bf3" + ); + assembly { + // deploy proxy using create2 + let proxy := create2(0, add(code, 0x20), mload(code), 0x0) + if iszero(extcodesize(proxy)) { + revert(0, 0) + } + } + } + } + + function getImplementSlot() public pure returns (bytes1 slot) { + assembly { + slot := implementation.slot + } + } + + function _upgrade(address _newImplementation) internal { + _upgradeBefore(_newImplementation); + + implementation = _newImplementation; + emit Upgraded(_newImplementation); + + _upgradeAfter(_newImplementation); + } + + function _upgradeBefore(address _newImplementation) internal virtual {} + + function _upgradeAfter(address _newImplementation) internal virtual {} +} diff --git a/assets/eip-7229/contracts/utils/Proxy1.sol b/assets/eip-7229/contracts/utils/Proxy1.sol new file mode 100644 index 0000000000000..bef3602dd8fb8 --- /dev/null +++ b/assets/eip-7229/contracts/utils/Proxy1.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +abstract contract Proxy1 { + address private implementation; + + event Upgraded(address indexed implementation); + + constructor(bool _deployProxy) { + if (_deployProxy) { + uint256 slot; + assembly { + slot := implementation.slot + } + require(slot <= 255, "implementation.slot is not within 255"); + // deploy proxy contract by logic contract + bytes memory code = abi.encodePacked( + hex"60", + getImplementSlot(), + hex"73", + address(this), + hex"8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3" + ); + assembly { + // deploy proxy using create2 + let proxy := create2(0, add(code, 0x20), mload(code), 0x0) + if iszero(extcodesize(proxy)) { + revert(0, 0) + } + } + } + } + + function getImplementSlot() public pure returns (bytes1 slot) { + assembly { + slot := implementation.slot + } + } + + function _upgrade(address _newImplementation) internal { + _upgradeBefore(_newImplementation); + + implementation = _newImplementation; + emit Upgraded(_newImplementation); + + _upgradeAfter(_newImplementation); + } + + function _upgradeBefore(address _newImplementation) internal virtual {} + + function _upgradeAfter(address _newImplementation) internal virtual {} +} diff --git a/assets/eip-7229/contracts/utils/Proxy32.sol b/assets/eip-7229/contracts/utils/Proxy32.sol new file mode 100644 index 0000000000000..894b4af37f45b --- /dev/null +++ b/assets/eip-7229/contracts/utils/Proxy32.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +abstract contract Proxy32 { + // keccak256("xiaobaiskill") - 1 + bytes32 private constant implementationSlot = + 0x62ea9ce5af089814ac46703c0a7a1a722768852e79429df8440425302a1dddcb; + + event Upgraded(address indexed implementation); + + constructor(bool _deployProxy) { + if (_deployProxy) { + // deploy proxy contract by logic contract + bytes memory code = abi.encodePacked( + hex"7f", + getImplementSlot(), + hex"73", + address(this), + hex"81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3" + ); + assembly { + let proxy := create2(0, add(code, 0x20), mload(code), 0x0) + if iszero(extcodesize(proxy)) { + revert(0, 0) + } + } + } + } + + function getImplementSlot() public pure returns (bytes32) { + return implementationSlot; + } + + function _upgrade(address _newImplementation) internal { + _upgradeBefore(_newImplementation); + + bytes32 slot = implementationSlot; + assembly { + sstore(slot, _newImplementation) + } + emit Upgraded(_newImplementation); + + _upgradeAfter(_newImplementation); + } + + function _upgradeBefore(address _newImplementation) internal virtual {} + + function _upgradeAfter(address _newImplementation) internal virtual {} +} diff --git a/assets/eip-7229/hardhat.config.ts b/assets/eip-7229/hardhat.config.ts new file mode 100644 index 0000000000000..28cb1cb059b63 --- /dev/null +++ b/assets/eip-7229/hardhat.config.ts @@ -0,0 +1,16 @@ +import * as dotenv from "dotenv"; + +import { HardhatUserConfig, task } from "hardhat/config"; +import "@nomiclabs/hardhat-etherscan"; +import "@nomiclabs/hardhat-waffle"; +import "@typechain/hardhat"; +import "hardhat-gas-reporter"; +import "solidity-coverage"; + +dotenv.config(); + +const config: HardhatUserConfig = { + solidity: "0.8.17", +}; + +export default config; diff --git a/assets/eip-7229/img/minimal-upgradable-proxy-1slot.png b/assets/eip-7229/img/minimal-upgradable-proxy-1slot.png new file mode 100644 index 0000000000000..f20aa6db20fba Binary files /dev/null and b/assets/eip-7229/img/minimal-upgradable-proxy-1slot.png differ diff --git a/assets/eip-7229/img/minimal-upgradable-proxy-32slot.png b/assets/eip-7229/img/minimal-upgradable-proxy-32slot.png new file mode 100644 index 0000000000000..76cd448514ea4 Binary files /dev/null and b/assets/eip-7229/img/minimal-upgradable-proxy-32slot.png differ diff --git a/assets/eip-7229/img/openzepplin-ERC-1967.png b/assets/eip-7229/img/openzepplin-ERC-1967.png new file mode 100644 index 0000000000000..48978ceb27d48 Binary files /dev/null and b/assets/eip-7229/img/openzepplin-ERC-1967.png differ diff --git a/assets/eip-7229/package.json b/assets/eip-7229/package.json new file mode 100644 index 0000000000000..ad011242472f8 --- /dev/null +++ b/assets/eip-7229/package.json @@ -0,0 +1,38 @@ +{ + "name": "hardhat-project", + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-etherscan": "^3.1.7", + "@nomiclabs/hardhat-waffle": "^2.0.6", + "@typechain/ethers-v5": "^7.2.0", + "@typechain/hardhat": "^2.3.1", + "@types/chai": "^4.3.5", + "@types/mocha": "^9.1.1", + "@types/node": "^12.20.55", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "chai": "^4.3.7", + "dotenv": "^10.0.0", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.8.0", + "eslint-config-standard": "^16.0.3", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^3.4.1", + "eslint-plugin-promise": "^5.2.0", + "ethereum-waffle": "^3.4.4", + "ethers": "^5.7.2", + "hardhat": "^2.16.0", + "hardhat-gas-reporter": "^1.0.9", + "prettier": "^2.8.8", + "prettier-plugin-solidity": "^1.1.3", + "solhint": "^3.4.1", + "solidity-coverage": "^0.7.22", + "ts-node": "^10.9.1", + "typechain": "^5.2.0", + "typescript": "^4.9.5" + }, + "dependencies": { + "web3": "^4.0.1" + } +} diff --git a/assets/eip-7229/test/mock0/example.ts b/assets/eip-7229/test/mock0/example.ts new file mode 100644 index 0000000000000..ae785884801c4 --- /dev/null +++ b/assets/eip-7229/test/mock0/example.ts @@ -0,0 +1,60 @@ +import { Example0V1 } from "../typechain/Example0V1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: Example0V1; +describe("example test for 0 slot", function () { + before( + "deploy minimal upgradable proxy when deploying logic contract", + async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("Example0V1"); + const v1 = await V1.deploy(); + await v1.deployed(); + console.log("logic payable contract", v1.address); + + // proxy's code + const code = pack( + ["bytes1", "address", "bytes"], + [ + "0x73", + v1.address, + "0x5f55600960285f396010603160093960195ff3365f5f375f5f365f5f545af43d5f5f3e3d5f82601757fd5bf3", + ] + ); + const proxyAddr = ethers.utils.getCreate2Address( + v1.address, + "0x0000000000000000000000000000000000000000000000000000000000000000", + ethers.utils.keccak256(code) + ); + console.log("proxy contract", proxyAddr); + proxyContract = v1.attach(proxyAddr); + } + ); + + it("update data", async function () { + // update data + await proxyContract.setNumber(11); + + expect(await proxyContract.number()).to.equal(11); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("Example1V2"); + const v2 = await V2.deploy(); + await v2.deployed(); + console.log("logic v2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + + v2.attach(proxyContract.address).addNumber(1); + + expect(await proxyContract.number()).to.equal(12); + }); +}); diff --git a/assets/eip-7229/test/mock0/payableToken.ts b/assets/eip-7229/test/mock0/payableToken.ts new file mode 100644 index 0000000000000..0e62eca190207 --- /dev/null +++ b/assets/eip-7229/test/mock0/payableToken.ts @@ -0,0 +1,126 @@ +import { PayableToken0V1 } from "../typechain/PayableToken0V1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: PayableToken0V1; +describe("pay to upgradable contract for 0 slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("PayableToken0V1"); + const v1 = await V1.deploy(); + await v1.deployed(); + console.log("logic PayableToken contract", v1.address); + + // deploy proxy contract + const code = pack( + ["bytes1", "address", "bytes"], + [ + "0x73", + v1.address, + "0x5f55600960285f396010603160093960195ff3365f5f375f5f365f5f545af43d5f5f3e3d5f82601757fd5bf3", + ] + ); + const Proxy = new ethers.ContractFactory( + "[]", + code.slice(2), + await ethers.getSigner() + ); + const proxy = await Proxy.deploy(); + await proxy.deployed(); + console.log("deploy proxy contract:", proxy.address); + proxyContract = v1.attach(proxy.address); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11, { value: ethers.utils.parseEther("1") }); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("1") + ); + }); + + it("receive & fallback", async function () { + // await ethers.provider.sendTransaction() + await ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("1"), + }); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("2") + ); + + expect(await proxyContract.number()).to.equal(12); + + await ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("2"), + data: "0x11112222", + }); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("4") + ); + + expect(await proxyContract.number()).to.equal(14); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("PayableToken0V2"); + const v2 = await V2.deploy(); + await v2.deployed(); + console.log("logic PayableTokenV2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + }); + + it("check receive & fallback after upgrade", async function () { + // await ethers.provider.sendTransaction() + await expect( + ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("1"), + }) + ).to.be.reverted; + + await expect( + ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("2"), + data: "0x11112222", + }) + ).to.be.reverted; + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("4") + ); + + expect(await proxyContract.number(), 14); + }); +}); diff --git a/assets/eip-7229/test/mock0/proxy.ts b/assets/eip-7229/test/mock0/proxy.ts new file mode 100644 index 0000000000000..a512cd5d304e3 --- /dev/null +++ b/assets/eip-7229/test/mock0/proxy.ts @@ -0,0 +1,74 @@ +import { Sim0V1 } from "../typechain/Sim0V1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: Sim0V1; +describe("Proxy for 0 slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("Sim0V1"); + const v1 = await V1.deploy(11); + await v1.deployed(); + console.log("logic v1 contract", v1.address); + + // deploy proxy contract + const code = pack( + ["bytes1", "address", "bytes"], + [ + "0x73", + v1.address, + "0x5f55600960285f396010603160093960195ff3365f5f375f5f365f5f545af43d5f5f3e3d5f82601757fd5bf3", + ] + ); + + const Proxy = new ethers.ContractFactory( + "[]", + code.slice(2), + await ethers.getSigner() + ); + const proxy = await Proxy.deploy(); + await proxy.deployed(); + console.log("deploy proxy contract:", proxy.address); + + // const deployedCode = await ethers.provider.getCode(proxy.address); + // console.log(deployedCode); + + const logicAddress = await ethers.provider.getStorageAt(proxy.address, 0); + console.log(logicAddress); + + proxyContract = v1.attach(proxy.address); + }); + + it("update data", async function () { + await proxyContract.init(); + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + + await proxyContract.setNumber(11); + expect(await proxyContract.number()).to.equal(11); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("Sim0V2"); + const v2 = await V2.deploy(1); + await v2.deployed(); + console.log("logic v2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + + v2.attach(proxyContract.address).addNumber(1); + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(12); + expect(await v2.attach(proxyContract.address).upgradeTime()).to.equal(0); + }); +}); diff --git a/assets/eip-7229/test/mock1/deployContract.ts b/assets/eip-7229/test/mock1/deployContract.ts new file mode 100644 index 0000000000000..28f18059a8c3f --- /dev/null +++ b/assets/eip-7229/test/mock1/deployContract.ts @@ -0,0 +1,39 @@ +import { SimV1 } from "./../typechain/SimV1.d"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: SimV1; +describe("deploy contract by contract for 1 bytes slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("Test1"); + const v1 = await V1.deploy(11); + await v1.deployed(); + console.log("logic v1 contract", v1.address); + + const Deploy = await ethers.getContractFactory("DeployContract1"); + const deploy = await Deploy.deploy(); + await deploy.deployed(); + + const tx = await deploy.createContract(v1.address); + await tx.wait(); + + const proxy = await deploy.precomputeContract(v1.address); + proxyContract = v1.attach(proxy); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + }); +}); diff --git a/assets/eip-7229/test/mock1/example.ts b/assets/eip-7229/test/mock1/example.ts new file mode 100644 index 0000000000000..386b62c9b3d3a --- /dev/null +++ b/assets/eip-7229/test/mock1/example.ts @@ -0,0 +1,62 @@ +import { Example1V1 } from "../typechain/Example1V1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: Example1V1; +describe("example test for 1 byte slot", function () { + before( + "deploy minimal upgradable proxy when deploying logic contract", + async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("Example1V1"); + const v1 = await V1.deploy(); + await v1.deployed(); + console.log("logic payable contract", v1.address); + + // proxy's code + const code = pack( + ["bytes1", "uint8", "bytes1", "address", "bytes"], + [ + "0x60", + await v1.getImplementSlot(), + "0x73", + v1.address, + "0x8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3", + ] + ); + const proxyAddr = ethers.utils.getCreate2Address( + v1.address, + "0x0000000000000000000000000000000000000000000000000000000000000000", + ethers.utils.keccak256(code) + ); + console.log("proxy contract", proxyAddr); + proxyContract = v1.attach(proxyAddr); + } + ); + + it("update data", async function () { + // update data + await proxyContract.setNumber(11); + + expect(await proxyContract.number()).to.equal(11); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("Example1V2"); + const v2 = await V2.deploy(); + await v2.deployed(); + console.log("logic v2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + + v2.attach(proxyContract.address).addNumber(1); + + expect(await proxyContract.number()).to.equal(12); + }); +}); diff --git a/assets/eip-7229/test/mock1/payableToken.ts b/assets/eip-7229/test/mock1/payableToken.ts new file mode 100644 index 0000000000000..bedb296789d71 --- /dev/null +++ b/assets/eip-7229/test/mock1/payableToken.ts @@ -0,0 +1,128 @@ +import { PayableToken } from "../typechain/PayableToken"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: PayableToken; +describe("pay to upgradable contract for 1 byte slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("PayableTokenV1"); + const v1 = await V1.deploy(); + await v1.deployed(); + console.log("logic PayableToken contract", v1.address); + + // deploy proxy contract + const code = pack( + ["bytes1", "uint8", "bytes1", "address", "bytes"], + [ + "0x60", + await v1.getImplementSlot(), + "0x73", + v1.address, + "0x8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3", + ] + ); + const Proxy = new ethers.ContractFactory( + "[]", + code.slice(2), + await ethers.getSigner() + ); + const proxy = await Proxy.deploy(); + await proxy.deployed(); + console.log("deploy proxy contract:", proxy.address); + proxyContract = v1.attach(proxy.address); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11, { value: ethers.utils.parseEther("1") }); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("1") + ); + }); + + it("receive & fallback", async function () { + // await ethers.provider.sendTransaction() + await ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("1"), + }); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("2") + ); + + expect(await proxyContract.number()).to.equal(12); + + await ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("2"), + data: "0x11112222", + }); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("4") + ); + + expect(await proxyContract.number()).to.equal(14); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("PayableTokenV2"); + const v2 = await V2.deploy(); + await v2.deployed(); + console.log("logic PayableTokenV2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + }); + + it("check receive & fallback after upgrade", async function () { + // await ethers.provider.sendTransaction() + await expect( + ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("1"), + }) + ).to.be.reverted; + + await expect( + ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("2"), + data: "0x11112222", + }) + ).to.be.reverted; + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("4") + ); + + expect(await proxyContract.number(), 14); + }); +}); diff --git a/assets/eip-7229/test/mock1/proxy.ts b/assets/eip-7229/test/mock1/proxy.ts new file mode 100644 index 0000000000000..5ad3c730d4b7d --- /dev/null +++ b/assets/eip-7229/test/mock1/proxy.ts @@ -0,0 +1,71 @@ +import { SimV1 } from "../typechain/SimV1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: SimV1; +describe("Proxy for 1 byte slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("SimV1"); + const v1 = await V1.deploy(11); + await v1.deployed(); + console.log("logic v1 contract", v1.address); + + // deploy proxy contract + const code = pack( + ["bytes1", "uint8", "bytes1", "address", "bytes"], + [ + "0x60", + await v1.getImplementSlot(), + "0x73", + v1.address, + "0x8155600960305f3960f81b60095260106039600a39601a5ff3365f5f375f5f365f60545af43d5f5f3e3d5f82601857fd5bf3", + ] + ); + const Proxy = new ethers.ContractFactory( + "[]", + code.slice(2), + await ethers.getSigner() + ); + const proxy = await Proxy.deploy(); + await proxy.deployed(); + console.log("deploy proxy contract:", proxy.address); + + proxyContract = v1.attach(proxy.address); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("SimV2"); + const v2 = await V2.deploy(1); + await v2.deployed(); + console.log("logic v2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + + v2.attach(proxyContract.address).addNumber(1); + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(12); + expect(await v2.attach(proxyContract.address).upgradeTime()).to.equal(0); + }); +}); diff --git a/assets/eip-7229/test/mock32/deployContract.ts b/assets/eip-7229/test/mock32/deployContract.ts new file mode 100644 index 0000000000000..5ffb849684fa1 --- /dev/null +++ b/assets/eip-7229/test/mock32/deployContract.ts @@ -0,0 +1,39 @@ +import { SimV1 } from "./../typechain/SimV1.d"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: SimV1; +describe("deploy contract by contract for 32 bytes slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("Test32"); + const v1 = await V1.deploy(11); + await v1.deployed(); + console.log("logic v1 contract", v1.address); + + const Deploy = await ethers.getContractFactory("DeployContract32"); + const deploy = await Deploy.deploy(); + await deploy.deployed(); + + const tx = await deploy.createContract(v1.address); + await tx.wait(); + + const proxy = await deploy.precomputeContract(v1.address); + proxyContract = v1.attach(proxy); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + }); +}); diff --git a/assets/eip-7229/test/mock32/example.ts b/assets/eip-7229/test/mock32/example.ts new file mode 100644 index 0000000000000..c23d4e5d137da --- /dev/null +++ b/assets/eip-7229/test/mock32/example.ts @@ -0,0 +1,62 @@ +import { Example32V1 } from "../typechain/Example32V1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: Example32V1; +describe("example test for 32 bytes slot", function () { + before( + "deploy minimal upgradable proxy when deploying logic contract", + async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("Example32V1"); + const v1 = await V1.deploy(); + await v1.deployed(); + console.log("logic payable contract", v1.address); + + // proxy's code + const code = pack( + ["bytes1", "uint256", "bytes1", "address", "bytes"], + [ + "0x7f", + await v1.getImplementSlot(), + "0x73", + v1.address, + "0x81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3", + ] + ); + const proxyAddr = ethers.utils.getCreate2Address( + v1.address, + "0x0000000000000000000000000000000000000000000000000000000000000000", + ethers.utils.keccak256(code) + ); + console.log("proxy contract", proxyAddr); + proxyContract = v1.attach(proxyAddr); + } + ); + + it("update data", async function () { + // update data + await proxyContract.setNumber(11); + + expect(await proxyContract.number()).to.equal(11); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("Example32V2"); + const v2 = await V2.deploy(); + await v2.deployed(); + console.log("logic v2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + + v2.attach(proxyContract.address).addNumber(1); + + expect(await proxyContract.number()).to.equal(12); + }); +}); diff --git a/assets/eip-7229/test/mock32/payableToken.ts b/assets/eip-7229/test/mock32/payableToken.ts new file mode 100644 index 0000000000000..fcc6b6e5ff4a3 --- /dev/null +++ b/assets/eip-7229/test/mock32/payableToken.ts @@ -0,0 +1,128 @@ +import { PayableToken } from "../typechain/PayableToken"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: PayableToken; +describe("pay to upgradable contract for 32 bytes slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("PayableTokenV1"); + const v1 = await V1.deploy(); + await v1.deployed(); + console.log("logic PayableToken contract", v1.address); + + // deploy proxy contract + const code = pack( + ["bytes1", "uint256", "bytes1", "address", "bytes"], + [ + "0x7f", + await v1.getImplementSlot(), + "0x73", + v1.address, + "0x81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3", + ] + ); + const Proxy = new ethers.ContractFactory( + "[]", + code.slice(2), + await ethers.getSigner() + ); + const proxy = await Proxy.deploy(); + await proxy.deployed(); + console.log("deploy proxy contract:", proxy.address); + proxyContract = v1.attach(proxy.address); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11, { value: ethers.utils.parseEther("1") }); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("1") + ); + }); + + it("receive & fallback", async function () { + // await ethers.provider.sendTransaction() + await ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("1"), + }); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("2") + ); + + expect(await proxyContract.number()).to.equal(12); + + await ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("2"), + data: "0x11112222", + }); + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("4") + ); + + expect(await proxyContract.number()).to.equal(14); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("PayableTokenV2"); + const v2 = await V2.deploy(); + await v2.deployed(); + console.log("logic PayableTokenV2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + }); + + it("check receive & fallback after upgrade", async function () { + // await ethers.provider.sendTransaction() + await expect( + ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("1"), + }) + ).to.be.reverted; + + await expect( + ( + await ethers.getSigner() + ).sendTransaction({ + to: proxyContract.address, + value: ethers.utils.parseEther("2"), + data: "0x11112222", + }) + ).to.be.reverted; + + expect( + await ethers.provider.getBalance(proxyContract.address), + ethers.utils.parseEther("4") + ); + + expect(await proxyContract.number(), 14); + }); +}); diff --git a/assets/eip-7229/test/mock32/proxy.ts b/assets/eip-7229/test/mock32/proxy.ts new file mode 100644 index 0000000000000..78b1903a01c60 --- /dev/null +++ b/assets/eip-7229/test/mock32/proxy.ts @@ -0,0 +1,72 @@ +import { SimV1 } from "../typechain/SimV1"; +import { Provider } from "@ethersproject/providers"; +import { Contract, Wallet } from "ethers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { pack } from "@ethersproject/solidity"; + +let proxyContract: SimV1; +describe("Proxy for 32 bytes slot", function () { + before("deploy minimal upgradable proxy", async function () { + // deploy SimV1 + const V1 = await ethers.getContractFactory("SimV1"); + const v1 = await V1.deploy(11); + await v1.deployed(); + console.log("logic v1 contract", v1.address); + + // deploy proxy contract + const code = pack( + ["bytes1", "uint256", "bytes1", "address", "bytes"], + [ + "0x7f", + await v1.getImplementSlot(), + "0x73", + v1.address, + "0x81556009604c3d396009526010605560293960395ff3365f5f375f5f365f7f545af43d5f5f3e3d5f82603757fd5bf3", + ] + ); + const Proxy = new ethers.ContractFactory( + "[]", + code.slice(2), + await ethers.getSigner() + ); + const proxy = await Proxy.deploy(); + await proxy.deployed(); + console.log("deploy proxy contract:", proxy.address); + + proxyContract = v1.attach(proxy.address); + }); + + it("update data", async function () { + // init & update data + await proxyContract.init(); + await proxyContract.setNumber(11); + + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(11); + }); + + it("upgrade", async function () { + // deploy SimV1 + const V2 = await ethers.getContractFactory("SimV2"); + const v2 = await V2.deploy(1); + await v2.deployed(); + console.log("logic v2 contract", v2.address); + + await expect(proxyContract.upgrade(v2.address)) + .to.emit(proxyContract, "Upgraded") + .withArgs(v2.address); + + v2.attach(proxyContract.address).addNumber(1); + // check proxy data + expect(await proxyContract.owner()).to.equal( + await (await ethers.getSigner()).getAddress() + ); + expect(await proxyContract.number()).to.equal(12); + + expect(await v2.attach(proxyContract.address).upgradeTime()).to.equal(0); + }); +});