Skip to content

Commit 7766d2f

Browse files
authored
Merge pull request #5 from TSxo/proxy
feat: UUPS
2 parents df80eb5 + 346ec8a commit 7766d2f

4 files changed

Lines changed: 108 additions & 25 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ name: CI
22

33
on:
44
push:
5+
branches: [ main ]
56
pull_request:
7+
branches: [ main ]
8+
types:
9+
- opened
10+
- edited
11+
- synchronize
612
workflow_dispatch:
713

814
env:

src/mocks/proxy/UUPS/UUPSImplementationMock.sol

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ pragma solidity ^0.8.20;
33

44
import { UUPSImplementation } from "@tsxo/libsol/proxy/UUPS/UUPSImplementation.sol";
55

6+
error Unauthorized();
7+
error AlreadyInitialized();
8+
69
contract UUPSCounterMock is UUPSImplementation {
710
uint256 private _count;
811
address private _owner;
@@ -19,7 +22,7 @@ contract UUPSCounterMock is UUPSImplementation {
1922
}
2023

2124
function initialize() external {
22-
require(_owner == address(0), "Already initialized");
25+
if (_owner != address(0)) revert AlreadyInitialized();
2326
_owner = msg.sender;
2427
}
2528

@@ -51,7 +54,7 @@ contract UUPSCounterMock is UUPSImplementation {
5154
}
5255

5356
function _authorizeUpgrade(address) internal override {
54-
require(msg.sender == _owner, "Not authorized");
57+
if (msg.sender != _owner) revert Unauthorized();
5558
}
5659
}
5760

@@ -96,7 +99,7 @@ contract UUPSCounterV2Mock is UUPSImplementation {
9699
emit Received(msg.value);
97100
}
98101

99-
function _authorizeUpgrade(address) internal override {
100-
require(msg.sender == _owner, "Not authorized");
102+
function _authorizeUpgrade(address) internal view override {
103+
if (msg.sender != _owner) revert Unauthorized();
101104
}
102105
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
/// @dev Does not implement proxiableUUID.
5+
contract NonCompliantContract { }
6+
7+
/// @dev Returns the wrong implementation slot.
8+
contract WrongImplementationSlotContract {
9+
function proxiableUUID() external pure returns (bytes32) {
10+
return bytes32(keccak256("WRONG_IMPLEMENTATION_SLOT"));
11+
}
12+
}

test/proxy/UUPS/UUPS.t.sol

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ pragma solidity ^0.8.20;
33

44
import { Test } from "forge-std/Test.sol";
55
import { UUPSProxy } from "@tsxo/libsol/proxy/UUPS/UUPSProxy.sol";
6+
import { ERC1967Logic } from "@tsxo/libsol/proxy/ERC1967/ERC1967Logic.sol";
67
import { UUPSCounterMock, UUPSCounterV2Mock } from "@tsxo/libsol/mocks/proxy/UUPS/UUPSImplementationMock.sol";
7-
import { console } from "forge-std/console.sol";
8+
import {
9+
NonCompliantContract,
10+
WrongImplementationSlotContract
11+
} from "@tsxo/libsol/mocks/proxy/UUPS/UUPSNonCompliantMock.sol";
812

913
interface TestEvents {
1014
event Upgraded(address indexed impl);
@@ -29,28 +33,34 @@ contract UUPSTest is Test, TestEvents {
2933

3034
function setUp() public {
3135
bytes memory initData = abi.encodeWithSignature("initialize()");
36+
3237
impl = new UUPSCounterMock();
3338
implV2 = new UUPSCounterV2Mock();
34-
3539
proxy = new UUPSProxy(address(impl), initData);
40+
3641
proxied = UUPSCounterMock(payable(address(proxy)));
3742
}
3843

3944
// -------------------------------------------------------------------------
40-
// Tests
45+
// Proxy Construction Tests
46+
47+
function test_ProxyConstructor_RejectsNonERC1822CompliantContracts() public {
48+
bytes4 err = ERC1967Logic.ERC1967Logic__UpgradeFailed.selector;
4149

42-
function test_ProxyConstructor_ERC1822Compliant() public {
4350
address wrongSlot = address(new WrongImplementationSlotContract());
4451
address nonCompliant = address(new NonCompliantContract());
4552

46-
vm.expectRevert();
53+
vm.expectRevert(err);
4754
new UUPSProxy(wrongSlot, "");
4855

4956
vm.expectRevert();
5057
new UUPSProxy(nonCompliant, "");
5158
}
5259

53-
function test_ProxyInitialization() public {
60+
// -------------------------------------------------------------------------
61+
// Proxy Initialization Tests
62+
63+
function test_ProxyInitialization() public view {
5464
bytes memory countCall = abi.encodeWithSignature("count()");
5565
(bool success, bytes memory data) = address(proxy).staticcall(countCall);
5666
uint256 count = abi.decode(data, (uint256));
@@ -59,7 +69,10 @@ contract UUPSTest is Test, TestEvents {
5969
assertEq(count, 0);
6070
}
6171

62-
function test_ProxyCalls() public {
72+
// -------------------------------------------------------------------------
73+
// Basic Functionality Tests
74+
75+
function test_ProxyCalls_DelegateToImplementation() public {
6376
proxied.increment();
6477
assertEq(proxied.count(), 1);
6578

@@ -72,7 +85,7 @@ contract UUPSTest is Test, TestEvents {
7285
assertEq(impl.count(), 0);
7386
}
7487

75-
function test_ProxyDelegatedCalls() public {
88+
function test_ProxyCalls_HandlesLowLevelCalls() public {
7689
bytes memory countCall = abi.encodeWithSignature("count()");
7790
bytes memory incCall = abi.encodeWithSignature("increment()");
7891
bytes memory decCall = abi.encodeWithSignature("decrement()");
@@ -118,6 +131,9 @@ contract UUPSTest is Test, TestEvents {
118131
assertFalse(success);
119132
}
120133

134+
// -------------------------------------------------------------------------
135+
// Ether Handling Tests
136+
121137
function test_ProxyEtherHandling() public {
122138
bytes memory fundCall = abi.encodeWithSignature("fundMe()");
123139
address user = address(0x1234);
@@ -147,6 +163,9 @@ contract UUPSTest is Test, TestEvents {
147163
vm.stopPrank();
148164
}
149165

166+
// -------------------------------------------------------------------------
167+
// Upgrade Tests
168+
150169
function test_Upgrade() public {
151170
proxied.setCount(5);
152171
assertEq(proxied.count(), 5);
@@ -194,7 +213,7 @@ contract UUPSTest is Test, TestEvents {
194213
assertEq(proxiedV2.count(), 5);
195214
}
196215

197-
function test_FailedUpgradeNotERC1822Compliant() public {
216+
function test_FailedUpgrade_NotERC1822Compliant() public {
198217
address nonCompliant = address(new NonCompliantContract());
199218

200219
bytes memory upgradeCall = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", nonCompliant, "");
@@ -203,7 +222,7 @@ contract UUPSTest is Test, TestEvents {
203222
assertFalse(success);
204223
}
205224

206-
function test_FailedUpgradeWrongImplementationSlot() public {
225+
function test_FailedUpgrade_WrongImplementationSlot() public {
207226
address wrongSlot = address(new WrongImplementationSlotContract());
208227

209228
bytes memory upgradeCall = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", wrongSlot, "");
@@ -212,7 +231,7 @@ contract UUPSTest is Test, TestEvents {
212231
assertFalse(success);
213232
}
214233

215-
function test_FailedUpgradeUnauthorized() public {
234+
function test_FailedUpgrade_Unauthorized() public {
216235
address attacker = address(0x5678);
217236
vm.startPrank(attacker);
218237

@@ -224,32 +243,75 @@ contract UUPSTest is Test, TestEvents {
224243
vm.stopPrank();
225244
}
226245

227-
function test_ProxiableUUID_DirectCallSucceeds() public {
246+
// -------------------------------------------------------------------------
247+
// Call Context
248+
249+
function test_ProxiableUUID_DirectCallSucceeds() public view {
228250
bytes32 actualSlot = impl.proxiableUUID();
229251
assertEq(actualSlot, IMPLEMENTATION_SLOT);
230252
}
231253

232-
function test_ProxiableUUID_ProxiedCallReverts() public {
254+
function test_ProxiableUUID_ProxiedCallReverts() public view {
233255
bytes memory proxiableUUIDCall = abi.encodeWithSignature("proxiableUUID()");
234256

235257
(bool success,) = address(proxy).staticcall(proxiableUUIDCall);
236258
assertFalse(success);
237259
}
238260

239-
function test_UpgradeToAndCall_CallReverts() public {
261+
function test_UpgradeToAndCall_DirectCallReverts() public {
240262
address tempImpl = address(new UUPSCounterV2Mock());
241263

242264
vm.expectRevert();
243265
impl.upgradeToAndCall(tempImpl, "");
244266
}
245-
}
246267

247-
/// @dev Does not implement proxiableUUID.
248-
contract NonCompliantContract { }
268+
// -------------------------------------------------------------------------
269+
// Fuzz Tests
249270

250-
/// @dev Returns the wrong implementation slot.
251-
contract WrongImplementationSlotContract {
252-
function proxiableUUID() external pure returns (bytes32) {
253-
return bytes32(keccak256("WRONG_IMPLEMENTATION_SLOT"));
271+
function testFuzz_SetCount(uint256 newCount) public {
272+
proxied.setCount(newCount);
273+
assertEq(impl.count(), 0);
274+
assertEq(proxied.count(), newCount);
275+
}
276+
277+
function testFuzz_MultipleUpgradesPreserveState(uint256 startCount) public {
278+
proxied.setCount(startCount);
279+
assertEq(proxied.count(), startCount);
280+
281+
bytes memory initCall = abi.encodeWithSignature("markAsUpgraded()");
282+
bytes memory upgradeCall = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", address(implV2), initCall);
283+
284+
(bool success,) = address(proxy).call(upgradeCall);
285+
assertTrue(success);
286+
287+
UUPSCounterV2Mock proxiedV2 = UUPSCounterV2Mock(payable(address(proxy)));
288+
assertEq(proxiedV2.count(), startCount);
289+
assertTrue(proxiedV2.upgraded());
290+
291+
UUPSCounterV2Mock implV3 = new UUPSCounterV2Mock();
292+
293+
upgradeCall = abi.encodeWithSignature("upgradeToAndCall(address,bytes)", address(implV3), initCall);
294+
(success,) = address(proxy).call(upgradeCall);
295+
assertTrue(success);
296+
297+
UUPSCounterV2Mock proxiedV3 = UUPSCounterV2Mock(payable(address(proxy)));
298+
assertEq(proxiedV3.count(), startCount);
299+
assertTrue(proxiedV3.upgraded());
300+
}
301+
302+
function testFuzz_EtherHandling(uint256 amount) public {
303+
vm.assume(amount > 0 && amount < 10 ether);
304+
305+
(bool success,) = address(proxy).call{ value: amount }("");
306+
assertTrue(success);
307+
assertEq(address(proxy).balance, amount);
308+
}
309+
310+
// -------------------------------------------------------------------------
311+
// Invariant Tests
312+
313+
function invariant_ImplementationSlotMatchesProxiableUUID() public view {
314+
bytes32 actualSlot = impl.proxiableUUID();
315+
assertEq(actualSlot, IMPLEMENTATION_SLOT);
254316
}
255317
}

0 commit comments

Comments
 (0)