Skip to content

Commit 2f8524c

Browse files
committed
Add testing for HybridProxy
1 parent db415aa commit 2f8524c

File tree

4 files changed

+92
-1
lines changed

4 files changed

+92
-1
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.27;
4+
5+
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
6+
7+
// Note using UUPSUpgradeable here will be too restrictive
8+
// - when upgrading an HydridProxy from beacon mode to UUPS mode, the `_checkProxy` check fails (implementation slot doesn't contain "self")
9+
// - when upgrading an HydridProxy from UUPS mode to beacon mode, the IERC1822Proxiable check fails (beacon doesn't implement it)
10+
// So we manually implement `upgradeToAndCall` without any checks
11+
contract UpgradeableImplementationMock {
12+
address private immutable __self = address(this);
13+
uint256 public immutable version;
14+
15+
error UnexpectedCall(address, uint256, bytes);
16+
17+
constructor(uint256 _version) {
18+
version = _version;
19+
}
20+
21+
function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual {
22+
ERC1967Utils.upgradeToAndCall(newImplementation, data);
23+
}
24+
25+
receive() external payable {
26+
revert UnexpectedCall(msg.sender, msg.value, "0x");
27+
}
28+
29+
fallback() external payable {
30+
revert UnexpectedCall(msg.sender, msg.value, msg.data);
31+
}
32+
}

contracts/mocks/import.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";

contracts/proxy/HybridProxy.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
1111
* @dev A version of an ERC-1967 proxy that uses the address stored in the implementation slot as a beacon.
1212
*
1313
* The design allows to set an initial beacon that the contract may quit by upgrading to its own implementation
14-
* afterwards.
14+
* afterwards. Transition between the "beacon mode" and the "direct mode" require implementation that expose an
15+
* upgrade mechanism that writes to the ERC-1967 implementation slot. Note that UUPSUpgradable includes security
16+
* checks that are not compatible with this proxy design.
1517
*
1618
* WARNING: The fallback mechanism relies on the implementation not to define the {IBeacon-implementation} function.
1719
* Consider that if your implementation has this function, it'll be assumed as the beacon address, meaning that

test/proxy/HybridProxy.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
async function fixture() {
6+
const [admin] = await ethers.getSigners();
7+
8+
const implementation1 = await ethers.deployContract('UpgradeableImplementationMock', [1]);
9+
const implementation2 = await ethers.deployContract('UpgradeableImplementationMock', [2]);
10+
const implementation3 = await ethers.deployContract('UpgradeableImplementationMock', [3]);
11+
const beacon = await ethers.deployContract('UpgradeableBeacon', [implementation1, admin]);
12+
const proxy = await ethers
13+
.deployContract('HybridProxy', [beacon, '0x'])
14+
.then(({ target }) => implementation1.attach(target));
15+
16+
return { admin, beacon, proxy, implementation1, implementation2, implementation3 };
17+
}
18+
19+
describe('HybridProxy', function () {
20+
beforeEach(async function () {
21+
Object.assign(this, await loadFixture(fixture));
22+
});
23+
24+
it('setup at construction', async function () {
25+
const data = ethers.randomBytes(128);
26+
await expect(ethers.deployContract('HybridProxy', [this.beacon, data]))
27+
.to.be.revertedWithCustomError(this.implementation1, 'UnexpectedCall')
28+
.withArgs(this.admin, 0, data);
29+
});
30+
31+
it('forwards calls', async function () {
32+
await expect(this.implementation1.attach(this.proxy).version()).to.eventually.equal(1);
33+
});
34+
35+
it('beacon upgrade', async function () {
36+
await this.beacon.upgradeTo(this.implementation2);
37+
await expect(this.implementation1.attach(this.proxy).version()).to.eventually.equal(2);
38+
});
39+
40+
it('decouple/recouple', async function () {
41+
await this.proxy.upgradeToAndCall(this.implementation3, '0x');
42+
await expect(this.implementation1.attach(this.proxy).version()).to.eventually.equal(3);
43+
44+
// beacon updated no longer affect the upgrade process
45+
await this.beacon.upgradeTo(this.implementation2);
46+
await expect(this.implementation1.attach(this.proxy).version()).to.eventually.equal(3);
47+
48+
// recouple to beacon
49+
await this.proxy.upgradeToAndCall(this.beacon, '0x');
50+
await expect(this.implementation1.attach(this.proxy).version()).to.eventually.equal(2);
51+
});
52+
});

0 commit comments

Comments
 (0)