diff --git a/src/DirectDemocracyVoting.sol b/src/DirectDemocracyVoting.sol index f4bbe9b..38d6cb6 100644 --- a/src/DirectDemocracyVoting.sol +++ b/src/DirectDemocracyVoting.sol @@ -126,7 +126,10 @@ contract DirectDemocracyVoting is Initializable { event QuorumPercentageSet(uint8 pct); /* ─────────── Initialiser ─────────── */ - constructor() initializer {} + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } function initialize( address hats_, diff --git a/src/EducationHub.sol b/src/EducationHub.sol index 50d4edb..c2ecf80 100644 --- a/src/EducationHub.sol +++ b/src/EducationHub.sol @@ -77,6 +77,11 @@ contract EducationHub is Initializable, ContextUpgradeable, ReentrancyGuardUpgra event TokenSet(address indexed newToken); event HatsSet(address indexed newHats); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /*────────── Initialiser ────────*/ function initialize( address tokenAddr, diff --git a/src/Executor.sol b/src/Executor.sol index b1b6b99..0d1d8c7 100644 --- a/src/Executor.sol +++ b/src/Executor.sol @@ -62,6 +62,11 @@ contract Executor is Initializable, OwnableUpgradeable, PausableUpgradeable, Ree event HatMinterAuthorized(address indexed minter, bool authorized); event HatsMinted(address indexed user, uint256[] hatIds); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /* ─────────── Initialiser ─────────── */ function initialize(address owner_, address hats_) external initializer { if (owner_ == address(0) || hats_ == address(0)) revert ZeroAddress(); diff --git a/src/HybridVoting.sol b/src/HybridVoting.sol index 15aa819..97429ee 100644 --- a/src/HybridVoting.sol +++ b/src/HybridVoting.sol @@ -117,7 +117,10 @@ contract HybridVoting is Initializable { event QuorumSet(uint8 pct); /* ─────── Initialiser ─────── */ - constructor() initializer {} + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } function initialize( address hats_, diff --git a/src/ImplementationRegistry.sol b/src/ImplementationRegistry.sol index 869415e..ba8ea4c 100644 --- a/src/ImplementationRegistry.sol +++ b/src/ImplementationRegistry.sol @@ -50,6 +50,11 @@ contract ImplementationRegistry is Initializable, OwnableUpgradeable { bool latest ); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /*───────── Initializer ─────────────*/ function initialize(address owner) external initializer { __Ownable_init(owner); diff --git a/src/OrgDeployer.sol b/src/OrgDeployer.sol index 2d522da..d5da0c3 100644 --- a/src/OrgDeployer.sol +++ b/src/OrgDeployer.sol @@ -140,7 +140,10 @@ contract OrgDeployer is Initializable { /*════════════════ INITIALIZATION ════════════════*/ - constructor() initializer {} + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } function initialize( address _governanceFactory, diff --git a/src/OrgRegistry.sol b/src/OrgRegistry.sol index 2033de0..937b57c 100644 --- a/src/OrgRegistry.sol +++ b/src/OrgRegistry.sol @@ -92,7 +92,9 @@ contract OrgRegistry is Initializable, OwnableUpgradeable { event OrgMetadataAdminHatSet(bytes32 indexed orgId, uint256 hatId); /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} + constructor() { + _disableInitializers(); + } /** * @dev Initializes the contract, replacing the constructor for upgradeable pattern diff --git a/src/ParticipationToken.sol b/src/ParticipationToken.sol index 04fdce7..fce7eca 100644 --- a/src/ParticipationToken.sol +++ b/src/ParticipationToken.sol @@ -70,6 +70,11 @@ contract ParticipationToken is Initializable, ERC20VotesUpgradeable, ReentrancyG event MemberHatSet(uint256 hat, bool allowed); event ApproverHatSet(uint256 hat, bool allowed); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /*─────────── Initialiser ──────*/ function initialize( address executor_, diff --git a/src/PaymasterHub.sol b/src/PaymasterHub.sol index db41f4a..75dc2bb 100644 --- a/src/PaymasterHub.sol +++ b/src/PaymasterHub.sol @@ -1409,7 +1409,8 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG function _authorizeUpgrade(address newImplementation) internal override { MainStorage storage main = _getMainStorage(); if (msg.sender != main.poaManager) revert NotPoaManager(); - // newImplementation is intentionally not validated to allow flexibility + if (newImplementation == address(0)) revert ZeroAddress(); + if (newImplementation.code.length == 0) revert ContractNotDeployed(); } // ============ Internal Functions ============ @@ -1762,10 +1763,4 @@ contract PaymasterHub is IPaymaster, Initializable, UUPSUpgradeable, ReentrancyG receive() external payable { emit BountyFunded(msg.value, address(this).balance); } - - /** - * @dev Storage gap for future upgrades - * Reserves 50 storage slots for new variables in future versions - */ - uint256[50] private __gap; } diff --git a/src/PaymentManager.sol b/src/PaymentManager.sol index 5fdf431..fd72a30 100644 --- a/src/PaymentManager.sol +++ b/src/PaymentManager.sol @@ -63,6 +63,11 @@ contract PaymentManager is IPaymentManager, Initializable, OwnableUpgradeable, R INITIALIZER ──────────────────────────────────────────────────────────────────────────*/ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /** * @notice Initializes the PaymentManager * @param _owner The address that will own the contract (typically the Executor) diff --git a/src/PoaManager.sol b/src/PoaManager.sol index 2c5a2ff..bdb16fb 100644 --- a/src/PoaManager.sol +++ b/src/PoaManager.sol @@ -72,6 +72,7 @@ contract PoaManager is Ownable(msg.sender) { /*──────────── Admin: add & bootstrap ───────────*/ function addContractType(string calldata typeName, address impl) external onlyOwner { if (impl == address(0)) revert ImplZero(); + if (impl.code.length == 0) revert ImplZero(); bytes32 tId = _id(typeName); if (address(beacons[tId]) != address(0)) revert TypeExists(); @@ -91,6 +92,7 @@ contract PoaManager is Ownable(msg.sender) { /*──────────── Admin: upgrade ───────────*/ function upgradeBeacon(string calldata typeName, address newImpl, string calldata version) external onlyOwner { if (newImpl == address(0)) revert ImplZero(); + if (newImpl.code.length == 0) revert ImplZero(); bytes32 tId = _id(typeName); UpgradeableBeacon beacon = beacons[tId]; if (address(beacon) == address(0)) revert TypeUnknown(); diff --git a/src/QuickJoin.sol b/src/QuickJoin.sol index 5b62163..1cd57b4 100644 --- a/src/QuickJoin.sol +++ b/src/QuickJoin.sol @@ -85,6 +85,11 @@ contract QuickJoin is Initializable, ContextUpgradeable, ReentrancyGuardUpgradea address indexed master, address indexed account, string username, bytes32 indexed credentialId, uint256[] hatIds ); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /* ───────── Initialiser ───── */ function initialize( address executor_, diff --git a/src/TaskManager.sol b/src/TaskManager.sol index 9d316ec..41a5cec 100644 --- a/src/TaskManager.sol +++ b/src/TaskManager.sol @@ -168,6 +168,11 @@ contract TaskManager is Initializable, ContextUpgradeable { event TaskApplicationApproved(uint256 indexed id, address indexed applicant, address indexed approver); event ExecutorUpdated(address newExecutor); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /*──────── Initialiser ───────*/ function initialize( address tokenAddress, diff --git a/src/UniversalAccountRegistry.sol b/src/UniversalAccountRegistry.sol index 7e77a22..13ba7c6 100644 --- a/src/UniversalAccountRegistry.sol +++ b/src/UniversalAccountRegistry.sol @@ -41,6 +41,11 @@ contract UniversalAccountRegistry is Initializable, OwnableUpgradeable { event UserDeleted(address indexed user, string oldUsername); event BatchRegistered(uint256 count); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /*────────────────────────── Initializer ────────────────────────────*/ function initialize(address initialOwner) external initializer { if (initialOwner == address(0)) revert InvalidChars(); diff --git a/test/EducationHub.t.sol b/test/EducationHub.t.sol index 67b9034..40cf439 100644 --- a/test/EducationHub.t.sol +++ b/test/EducationHub.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import {EducationHub, IParticipationToken} from "../src/EducationHub.sol"; import {ValidationLib} from "../src/libs/ValidationLib.sol"; @@ -63,7 +65,9 @@ contract MockPT is Test, IParticipationToken { hats.mintHat(MEMBER_HAT, creator); // creator is also a member hats.mintHat(MEMBER_HAT, learner); - hub = new EducationHub(); + EducationHub _hubImpl = new EducationHub(); + UpgradeableBeacon _hubBeacon = new UpgradeableBeacon(address(_hubImpl), address(this)); + hub = EducationHub(address(new BeaconProxy(address(_hubBeacon), ""))); uint256[] memory creatorHats = new uint256[](1); creatorHats[0] = CREATOR_HAT; uint256[] memory memberHats = new uint256[](1); @@ -87,7 +91,9 @@ contract MockPT is Test, IParticipationToken { } function testInitializeZeroAddressReverts() public { - EducationHub tmp = new EducationHub(); + EducationHub _tmpImpl = new EducationHub(); + UpgradeableBeacon _tmpBeacon = new UpgradeableBeacon(address(_tmpImpl), address(this)); + EducationHub tmp = EducationHub(address(new BeaconProxy(address(_tmpBeacon), ""))); uint256[] memory creatorHats = new uint256[](0); uint256[] memory memberHats = new uint256[](0); vm.expectRevert(EducationHub.ZeroAddress.selector); diff --git a/test/Executor.t.sol b/test/Executor.t.sol index c90a6e3..0b6819b 100644 --- a/test/Executor.t.sol +++ b/test/Executor.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "../src/Executor.sol"; import "./mocks/MockHats.sol"; @@ -22,7 +24,9 @@ contract ExecutorTest is Test { function setUp() public { hats = new MockHats(); - exec = new Executor(); + Executor impl = new Executor(); + UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), address(this)); + exec = Executor(payable(address(new BeaconProxy(address(beacon), "")))); exec.initialize(owner, address(hats)); target = new Target(); exec.setCaller(caller); diff --git a/test/ImplementationRegistry.t.sol b/test/ImplementationRegistry.t.sol index 1cb5b2a..d2cc2cc 100644 --- a/test/ImplementationRegistry.t.sol +++ b/test/ImplementationRegistry.t.sol @@ -2,13 +2,17 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "../src/ImplementationRegistry.sol"; contract ImplementationRegistryTest is Test { ImplementationRegistry reg; function setUp() public { - reg = new ImplementationRegistry(); + ImplementationRegistry _regImpl = new ImplementationRegistry(); + UpgradeableBeacon _regBeacon = new UpgradeableBeacon(address(_regImpl), address(this)); + reg = ImplementationRegistry(address(new BeaconProxy(address(_regBeacon), ""))); reg.initialize(address(this)); } diff --git a/test/Passkey.t.sol b/test/Passkey.t.sol index 10a7f86..b76ac0c 100644 --- a/test/Passkey.t.sol +++ b/test/Passkey.t.sol @@ -157,7 +157,9 @@ contract PasskeyTest is Test { mockExecutor = new MockExecutor(); // Deploy account registry - accountRegistry = new UniversalAccountRegistry(); + UniversalAccountRegistry _uarImpl = new UniversalAccountRegistry(); + UpgradeableBeacon _uarBeacon = new UpgradeableBeacon(address(_uarImpl), owner); + accountRegistry = UniversalAccountRegistry(address(new BeaconProxy(address(_uarBeacon), ""))); accountRegistry.initialize(owner); vm.stopPrank(); diff --git a/test/PaymentManagerMerkle.t.sol b/test/PaymentManagerMerkle.t.sol index 4e82642..7e04b12 100644 --- a/test/PaymentManagerMerkle.t.sol +++ b/test/PaymentManagerMerkle.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import {console2} from "forge-std/console2.sol"; import {PaymentManager} from "../src/PaymentManager.sol"; import {ParticipationToken} from "../src/ParticipationToken.sol"; @@ -50,7 +52,9 @@ contract PaymentManagerMerkleTest is Test { hats = new MockHats(); // Deploy participation token - participationToken = new ParticipationToken(); + ParticipationToken _ptImpl = new ParticipationToken(); + UpgradeableBeacon _ptBeacon = new UpgradeableBeacon(address(_ptImpl), address(this)); + participationToken = ParticipationToken(address(new BeaconProxy(address(_ptBeacon), ""))); uint256[] memory memberHats = new uint256[](1); memberHats[0] = memberHatId; @@ -60,7 +64,9 @@ contract PaymentManagerMerkleTest is Test { participationToken.initialize(executor, "Participation Token", "PART", address(hats), memberHats, approverHats); // Deploy payment manager - paymentManager = new PaymentManager(); + PaymentManager _pmImpl = new PaymentManager(); + UpgradeableBeacon _pmBeacon = new UpgradeableBeacon(address(_pmImpl), address(this)); + paymentManager = PaymentManager(payable(address(new BeaconProxy(address(_pmBeacon), "")))); paymentManager.initialize(executor, address(participationToken)); // Deploy payment token diff --git a/test/PoaManager.t.sol b/test/PoaManager.t.sol index 0db4be4..601f4ba 100644 --- a/test/PoaManager.t.sol +++ b/test/PoaManager.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "../src/PoaManager.sol"; import "../src/ImplementationRegistry.sol"; @@ -16,7 +18,9 @@ contract PoaManagerTest is Test { address owner = address(this); function setUp() public { - reg = new ImplementationRegistry(); + ImplementationRegistry _regImpl = new ImplementationRegistry(); + UpgradeableBeacon _regBeacon = new UpgradeableBeacon(address(_regImpl), address(this)); + reg = ImplementationRegistry(address(new BeaconProxy(address(_regBeacon), ""))); reg.initialize(owner); pm = new PoaManager(address(reg)); reg.transferOwnership(address(pm)); diff --git a/test/QuickJoin.t.sol b/test/QuickJoin.t.sol index 01a9055..18d3e3f 100644 --- a/test/QuickJoin.t.sol +++ b/test/QuickJoin.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.21; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "../src/QuickJoin.sol"; import "./mocks/MockHats.sol"; @@ -56,7 +58,9 @@ contract QuickJoinTest is Test { hats = new MockHats(); registry = new MockRegistry(); mockExecutor = new MockExecutorHatMinter(); - qj = new QuickJoin(); + QuickJoin _qjImpl = new QuickJoin(); + UpgradeableBeacon _qjBeacon = new UpgradeableBeacon(address(_qjImpl), address(this)); + qj = QuickJoin(address(new BeaconProxy(address(_qjBeacon), ""))); uint256[] memory memberHats = new uint256[](1); memberHats[0] = DEFAULT_HAT_ID; @@ -76,7 +80,9 @@ contract QuickJoinTest is Test { } function testInitializeZeroAddressReverts() public { - QuickJoin tmp = new QuickJoin(); + QuickJoin _tmpImpl = new QuickJoin(); + UpgradeableBeacon _tmpBeacon = new UpgradeableBeacon(address(_tmpImpl), address(this)); + QuickJoin tmp = QuickJoin(address(new BeaconProxy(address(_tmpBeacon), ""))); uint256[] memory memberHats = new uint256[](1); memberHats[0] = DEFAULT_HAT_ID; vm.expectRevert(QuickJoin.InvalidAddress.selector); diff --git a/test/TaskManager.t.sol b/test/TaskManager.t.sol index 44c34de..3108b66 100644 --- a/test/TaskManager.t.sol +++ b/test/TaskManager.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "forge-std/console.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -154,7 +156,9 @@ contract MockToken is Test, IERC20 { setHat(pm1, PM_HAT); setHat(member1, MEMBER_HAT); - tm = new TaskManager(); + TaskManager _tmImpl = new TaskManager(); + UpgradeableBeacon _tmBeacon = new UpgradeableBeacon(address(_tmImpl), address(this)); + tm = TaskManager(address(new BeaconProxy(address(_tmBeacon), ""))); lens = new TaskManagerLens(); uint256[] memory creatorHats = _hatArr(CREATOR_HAT); @@ -5440,7 +5444,9 @@ contract MockToken is Test, IERC20 { hats.mintHat(PM_HAT, pm1); hats.mintHat(MEMBER_HAT, member1); - tm = new TaskManager(); + TaskManager _tmImpl = new TaskManager(); + UpgradeableBeacon _tmBeacon = new UpgradeableBeacon(address(_tmImpl), address(this)); + tm = TaskManager(address(new BeaconProxy(address(_tmBeacon), ""))); lens = new TaskManagerLens(); uint256[] memory creatorHats = new uint256[](1); creatorHats[0] = CREATOR_HAT; diff --git a/test/UniversalAccountRegistry.t.sol b/test/UniversalAccountRegistry.t.sol index 10217a2..f5d75cc 100644 --- a/test/UniversalAccountRegistry.t.sol +++ b/test/UniversalAccountRegistry.t.sol @@ -2,6 +2,8 @@ pragma solidity ^0.8.20; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; import "../src/UniversalAccountRegistry.sol"; contract UARTest is Test { @@ -9,7 +11,9 @@ contract UARTest is Test { address user = address(1); function setUp() public { - reg = new UniversalAccountRegistry(); + UniversalAccountRegistry _regImpl = new UniversalAccountRegistry(); + UpgradeableBeacon _regBeacon = new UpgradeableBeacon(address(_regImpl), address(this)); + reg = UniversalAccountRegistry(address(new BeaconProxy(address(_regBeacon), ""))); reg.initialize(address(this)); } diff --git a/test/UpgradeEdgeCases.t.sol b/test/UpgradeEdgeCases.t.sol new file mode 100644 index 0000000..0f4de06 --- /dev/null +++ b/test/UpgradeEdgeCases.t.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import {SwitchableBeacon} from "../src/SwitchableBeacon.sol"; + +/// @title UpgradeEdgeCasesTest +/// @notice Tests the full upgrade chain: PoaBeacon → SwitchableBeacon → BeaconProxy → delegatecall +/// with real proxy state, mode switches, multi-tenancy, and recovery scenarios. +contract UpgradeEdgeCasesTest is Test { + UpgradeableBeacon poaBeacon; + MockUpgradeableV1 implV1; + MockUpgradeableV2 implV2; + MockUpgradeableV3 implV3; + + function setUp() public { + implV1 = new MockUpgradeableV1(); + implV2 = new MockUpgradeableV2(); + implV3 = new MockUpgradeableV3(); + poaBeacon = new UpgradeableBeacon(address(implV1), address(this)); + } + + /// @dev Helper: create a SwitchableBeacon in Mirror mode + a BeaconProxy using it + function _deployMirrorProxy() internal returns (SwitchableBeacon switchable, MockUpgradeableV1 proxy) { + switchable = new SwitchableBeacon(address(this), address(poaBeacon), address(0), SwitchableBeacon.Mode.Mirror); + BeaconProxy bp = new BeaconProxy(address(switchable), ""); + proxy = MockUpgradeableV1(address(bp)); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 1: Full upgrade chain flows through Mirror to proxy + // ══════════════════════════════════════════════════════════════════════ + + function testPoaBeaconUpgradeFlowsThroughMirrorToProxy() public { + (SwitchableBeacon switchable, MockUpgradeableV1 proxy) = _deployMirrorProxy(); + + // Write state through V1 + proxy.setValue(42); + assertEq(proxy.value(), 42); + assertEq(proxy.version(), 1); + + // Upgrade POA beacon to V2 + poaBeacon.upgradeTo(address(implV2)); + + // Verify chain: proxy → switchable → poaBeacon → V2 + assertEq(switchable.implementation(), address(implV2)); + + // State preserved, V2 features available + MockUpgradeableV2 proxyV2 = MockUpgradeableV2(address(proxy)); + assertEq(proxyV2.value(), 42); + assertEq(proxyV2.version(), 2); + proxyV2.setNewField(99); + assertEq(proxyV2.newField(), 99); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 2: Pinned proxy ignores POA upgrade + // ══════════════════════════════════════════════════════════════════════ + + function testPinnedProxyIgnoresPoaUpgrade() public { + (SwitchableBeacon switchable, MockUpgradeableV1 proxy) = _deployMirrorProxy(); + + proxy.setValue(77); + assertEq(proxy.version(), 1); + + // Pin to current V1 + switchable.pinToCurrent(); + assertFalse(switchable.isMirrorMode()); + + // Upgrade POA beacon to V2 + poaBeacon.upgradeTo(address(implV2)); + + // Proxy still on V1 + assertEq(proxy.version(), 1); + assertEq(proxy.value(), 77); + assertEq(switchable.implementation(), address(implV1)); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 3: Two proxies with divergent modes (multi-tenancy) + // ══════════════════════════════════════════════════════════════════════ + + function testTwoProxiesDivergentModes() public { + // Org1: Mirror mode + (SwitchableBeacon switchable1, MockUpgradeableV1 proxy1) = _deployMirrorProxy(); + // Org2: starts Mirror, will pin + (SwitchableBeacon switchable2, MockUpgradeableV1 proxy2) = _deployMirrorProxy(); + + // Write distinct state + proxy1.setValue(10); + proxy2.setValue(20); + + // Org2 pins to current V1 + switchable2.pinToCurrent(); + + // Upgrade POA beacon to V2 + poaBeacon.upgradeTo(address(implV2)); + + // Org1 (mirror) → V2 + MockUpgradeableV2 proxy1V2 = MockUpgradeableV2(address(proxy1)); + assertEq(proxy1V2.version(), 2); + assertEq(proxy1V2.value(), 10); + + // Org2 (pinned) → still V1 + assertEq(proxy2.version(), 1); + assertEq(proxy2.value(), 20); + + // No cross-contamination + assertEq(switchable1.implementation(), address(implV2)); + assertEq(switchable2.implementation(), address(implV1)); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 4: State preserved through Mirror → Static → Mirror cycle + // ══════════════════════════════════════════════════════════════════════ + + function testProxyStatePreservedThroughMirrorStaticMirrorCycle() public { + (SwitchableBeacon switchable, MockUpgradeableV1 proxy) = _deployMirrorProxy(); + + // 1. Mirror mode, V1 + proxy.setValue(42); + assertEq(proxy.value(), 42); + assertEq(proxy.version(), 1); + + // 2. Pin → Static + switchable.pinToCurrent(); + assertFalse(switchable.isMirrorMode()); + + // 3. Upgrade POA to V2 (proxy should NOT see it) + poaBeacon.upgradeTo(address(implV2)); + assertEq(proxy.version(), 1); + assertEq(proxy.value(), 42); + + // 4. Back to Mirror + switchable.setMirror(address(poaBeacon)); + assertTrue(switchable.isMirrorMode()); + + // 5. Now on V2, state preserved + MockUpgradeableV2 proxyV2 = MockUpgradeableV2(address(proxy)); + assertEq(proxyV2.version(), 2); + assertEq(proxyV2.value(), 42); + + // V2 features work + proxyV2.setNewField(100); + assertEq(proxyV2.newField(), 100); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 5: pinToCurrent reverts when mirror returns zero + // ══════════════════════════════════════════════════════════════════════ + + function testPinToCurrentWhenMirrorReturnsZero() public { + MockBrokenBeacon brokenBeacon = new MockBrokenBeacon(); + + SwitchableBeacon switchable = + new SwitchableBeacon(address(this), address(brokenBeacon), address(0), SwitchableBeacon.Mode.Mirror); + + // pinToCurrent should revert - mirror returns address(0) + vm.expectRevert(SwitchableBeacon.ImplNotSet.selector); + switchable.pinToCurrent(); + + // State unchanged - still in Mirror mode + assertTrue(switchable.isMirrorMode()); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 6: Recovery from broken mirror via pin to known good + // ══════════════════════════════════════════════════════════════════════ + + function testRecoveryFromBrokenMirrorViaPinToKnownGood() public { + MockBrokenBeacon brokenBeacon = new MockBrokenBeacon(); + + SwitchableBeacon switchable = + new SwitchableBeacon(address(this), address(brokenBeacon), address(0), SwitchableBeacon.Mode.Mirror); + + // implementation() reverts + vm.expectRevert(SwitchableBeacon.ImplNotSet.selector); + switchable.implementation(); + + // tryGetImplementation returns failure + (bool success,) = switchable.tryGetImplementation(); + assertFalse(success); + + // Recovery: pin to known-good implementation + switchable.pin(address(implV1)); + assertEq(switchable.implementation(), address(implV1)); + assertFalse(switchable.isMirrorMode()); + + // Proxy using this beacon now works + BeaconProxy bp = new BeaconProxy(address(switchable), ""); + MockUpgradeableV1 proxy = MockUpgradeableV1(address(bp)); + proxy.setValue(55); + assertEq(proxy.value(), 55); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 7: Multiple sequential upgrades preserve state + // ══════════════════════════════════════════════════════════════════════ + + function testMultipleSequentialUpgradesPreserveState() public { + (, MockUpgradeableV1 proxy) = _deployMirrorProxy(); + + // V1: set value + proxy.setValue(10); + assertEq(proxy.version(), 1); + + // Upgrade to V2, set V2 field + poaBeacon.upgradeTo(address(implV2)); + MockUpgradeableV2 proxyV2 = MockUpgradeableV2(address(proxy)); + assertEq(proxyV2.version(), 2); + assertEq(proxyV2.value(), 10); // V1 state preserved + proxyV2.setNewField(20); + + // Upgrade to V3, set V3 field + poaBeacon.upgradeTo(address(implV3)); + MockUpgradeableV3 proxyV3 = MockUpgradeableV3(address(proxy)); + assertEq(proxyV3.version(), 3); + assertEq(proxyV3.value(), 10); // V1 state preserved + assertEq(proxyV3.newField(), 20); // V2 state preserved + proxyV3.setThirdField(30); + assertEq(proxyV3.thirdField(), 30); + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 8: Pin to arbitrary impl not from mirror + // ══════════════════════════════════════════════════════════════════════ + + function testPinToArbitraryImplNotFromMirror() public { + (SwitchableBeacon switchable, MockUpgradeableV1 proxy) = _deployMirrorProxy(); + + proxy.setValue(33); + assertEq(proxy.version(), 1); + + // Pin directly to V3 (never served by mirror, which has V1) + switchable.pin(address(implV3)); + assertEq(switchable.implementation(), address(implV3)); + assertFalse(switchable.isMirrorMode()); + + // Proxy now uses V3 + MockUpgradeableV3 proxyV3 = MockUpgradeableV3(address(proxy)); + assertEq(proxyV3.version(), 3); + assertEq(proxyV3.value(), 33); // State preserved + + // POA beacon upgrade to V2 has no effect + poaBeacon.upgradeTo(address(implV2)); + assertEq(proxyV3.version(), 3); // Still pinned to V3 + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 9: setMirror to a different POA beacon + // ══════════════════════════════════════════════════════════════════════ + + function testSetMirrorToDifferentPoaBeacon() public { + // BeaconA has V1, BeaconB has V2 + UpgradeableBeacon beaconB = new UpgradeableBeacon(address(implV2), address(this)); + + (SwitchableBeacon switchable, MockUpgradeableV1 proxy) = _deployMirrorProxy(); + proxy.setValue(50); + assertEq(proxy.version(), 1); // tracking beaconA (V1) + + // Switch to tracking beaconB + switchable.setMirror(address(beaconB)); + MockUpgradeableV2 proxyV2 = MockUpgradeableV2(address(proxy)); + assertEq(proxyV2.version(), 2); // now on V2 + assertEq(proxyV2.value(), 50); // state preserved + + // Upgrade beaconA to V3 - no effect (tracking beaconB now) + poaBeacon.upgradeTo(address(implV3)); + assertEq(proxyV2.version(), 2); // still V2 + + // Upgrade beaconB to V3 - this one matters + beaconB.upgradeTo(address(implV3)); + MockUpgradeableV3 proxyV3 = MockUpgradeableV3(address(proxy)); + assertEq(proxyV3.version(), 3); + assertEq(proxyV3.value(), 50); // state preserved through all transitions + } + + // ══════════════════════════════════════════════════════════════════════ + // Test 10: Ownership transfer and subsequent pin + // ══════════════════════════════════════════════════════════════════════ + + function testBeaconOwnershipTransferAndSubsequentPin() public { + address factory = address(this); + address executor = address(0xE1E2); + + SwitchableBeacon switchable = + new SwitchableBeacon(factory, address(poaBeacon), address(0), SwitchableBeacon.Mode.Mirror); + + // Factory initiates transfer to executor + switchable.transferOwnership(executor); + assertEq(switchable.owner(), factory); + assertEq(switchable.pendingOwner(), executor); + + // Factory can still manage beacon (owner until accepted) + switchable.pin(address(implV1)); + assertEq(switchable.implementation(), address(implV1)); + switchable.setMirror(address(poaBeacon)); // back to mirror + + // Executor accepts ownership + vm.prank(executor); + switchable.acceptOwnership(); + assertEq(switchable.owner(), executor); + assertEq(switchable.pendingOwner(), address(0)); + + // Factory can no longer manage + vm.expectRevert(SwitchableBeacon.NotOwner.selector); + switchable.pin(address(implV2)); + + vm.expectRevert(SwitchableBeacon.NotOwner.selector); + switchable.setMirror(address(poaBeacon)); + + vm.expectRevert(SwitchableBeacon.NotOwner.selector); + switchable.pinToCurrent(); + + // Executor can manage + vm.prank(executor); + switchable.pin(address(implV2)); + assertEq(switchable.implementation(), address(implV2)); + } +} + +// ══════════════════════════════════════════════════════════════════════ +// Mock implementations with storage-compatible layout progression +// ══════════════════════════════════════════════════════════════════════ + +contract MockUpgradeableV1 { + uint256 public value; // slot 0 + + function setValue(uint256 v) external { + value = v; + } + + function version() external pure returns (uint256) { + return 1; + } +} + +contract MockUpgradeableV2 { + uint256 public value; // slot 0 (same as V1) + uint256 public newField; // slot 1 (new, doesn't overwrite) + + function setValue(uint256 v) external { + value = v; + } + + function setNewField(uint256 v) external { + newField = v; + } + + function version() external pure returns (uint256) { + return 2; + } +} + +contract MockUpgradeableV3 { + uint256 public value; // slot 0 + uint256 public newField; // slot 1 + uint256 public thirdField; // slot 2 + + function setValue(uint256 v) external { + value = v; + } + + function setNewField(uint256 v) external { + newField = v; + } + + function setThirdField(uint256 v) external { + thirdField = v; + } + + function version() external pure returns (uint256) { + return 3; + } +} + +contract MockBrokenBeacon { + function implementation() external pure returns (address) { + return address(0); + } +} diff --git a/test/UpgradeSafety.t.sol b/test/UpgradeSafety.t.sol new file mode 100644 index 0000000..39601fb --- /dev/null +++ b/test/UpgradeSafety.t.sol @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; + +import {Executor} from "../src/Executor.sol"; +import {HybridVoting} from "../src/HybridVoting.sol"; +import {DirectDemocracyVoting} from "../src/DirectDemocracyVoting.sol"; +import {ParticipationToken} from "../src/ParticipationToken.sol"; +import {QuickJoin} from "../src/QuickJoin.sol"; +import {TaskManager} from "../src/TaskManager.sol"; +import {EducationHub} from "../src/EducationHub.sol"; +import {PaymentManager} from "../src/PaymentManager.sol"; +import {UniversalAccountRegistry} from "../src/UniversalAccountRegistry.sol"; +import {ImplementationRegistry} from "../src/ImplementationRegistry.sol"; +import {OrgRegistry} from "../src/OrgRegistry.sol"; +import {OrgDeployer} from "../src/OrgDeployer.sol"; +import {EligibilityModule} from "../src/EligibilityModule.sol"; +import {ToggleModule} from "../src/ToggleModule.sol"; +import {PasskeyAccount} from "../src/PasskeyAccount.sol"; +import {PasskeyAccountFactory} from "../src/PasskeyAccountFactory.sol"; +import {PaymasterHub} from "../src/PaymasterHub.sol"; +import {PoaManager} from "../src/PoaManager.sol"; +import {SwitchableBeacon} from "../src/SwitchableBeacon.sol"; + +/// @title UpgradeSafetyTest +/// @notice Comprehensive tests verifying upgrade safety invariants for all upgradeable contracts +contract UpgradeSafetyTest is Test { + address constant OWNER = address(0xA); + address constant HATS = address(0xB); + address constant UNAUTHORIZED = address(0xDEAD); + + // ══════════════════════════════════════════════════════════════════════ + // SECTION 1: Re-initialization prevention + // Every implementation contract must revert when initialize() is called + // ══════════════════════════════════════════════════════════════════════ + + function testExecutorImplCannotBeInitialized() public { + Executor impl = new Executor(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, HATS); + } + + function testParticipationTokenImplCannotBeInitialized() public { + ParticipationToken impl = new ParticipationToken(); + uint256[] memory hats = new uint256[](0); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, "Token", "TKN", HATS, hats, hats); + } + + function testTaskManagerImplCannotBeInitialized() public { + TaskManager impl = new TaskManager(); + uint256[] memory hats = new uint256[](0); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, HATS, hats, OWNER, OWNER); + } + + function testQuickJoinImplCannotBeInitialized() public { + QuickJoin impl = new QuickJoin(); + uint256[] memory hats = new uint256[](0); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, HATS, OWNER, OWNER, hats); + } + + function testEducationHubImplCannotBeInitialized() public { + EducationHub impl = new EducationHub(); + uint256[] memory hats = new uint256[](0); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, HATS, OWNER, hats, hats); + } + + function testPaymentManagerImplCannotBeInitialized() public { + PaymentManager impl = new PaymentManager(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, OWNER); + } + + function testUniversalAccountRegistryImplCannotBeInitialized() public { + UniversalAccountRegistry impl = new UniversalAccountRegistry(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER); + } + + function testImplementationRegistryImplCannotBeInitialized() public { + ImplementationRegistry impl = new ImplementationRegistry(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER); + } + + function testHybridVotingImplCannotBeInitialized() public { + HybridVoting impl = new HybridVoting(); + uint256[] memory hats = new uint256[](0); + address[] memory targets = new address[](0); + HybridVoting.ClassConfig[] memory classes = new HybridVoting.ClassConfig[](0); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(HATS, OWNER, hats, targets, 51, classes); + } + + function testDirectDemocracyVotingImplCannotBeInitialized() public { + DirectDemocracyVoting impl = new DirectDemocracyVoting(); + uint256[] memory hats = new uint256[](0); + address[] memory targets = new address[](0); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(HATS, OWNER, hats, hats, targets, 51); + } + + function testOrgRegistryImplCannotBeInitialized() public { + OrgRegistry impl = new OrgRegistry(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, HATS); + } + + function testEligibilityModuleImplCannotBeInitialized() public { + EligibilityModule impl = new EligibilityModule(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, HATS, OWNER); + } + + function testToggleModuleImplCannotBeInitialized() public { + ToggleModule impl = new ToggleModule(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER); + } + + function testPaymasterHubImplCannotBeInitialized() public { + PaymasterHub impl = new PaymasterHub(); + // PaymasterHub requires entryPoint to be a contract + address mockEntryPoint = address(new MockEntryPoint()); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(mockEntryPoint, HATS, OWNER); + } + + function testPasskeyAccountImplCannotBeInitialized() public { + PasskeyAccount impl = new PasskeyAccount(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, bytes32(uint256(1)), bytes32(uint256(2)), bytes32(uint256(3)), OWNER, 1 days); + } + + function testPasskeyAccountFactoryImplCannotBeInitialized() public { + PasskeyAccountFactory impl = new PasskeyAccountFactory(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, OWNER, OWNER, 1 days); + } + + function testOrgDeployerImplCannotBeInitialized() public { + OrgDeployer impl = new OrgDeployer(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER, OWNER, OWNER, OWNER, OWNER, HATS, OWNER, OWNER); + } + + // ══════════════════════════════════════════════════════════════════════ + // SECTION 2: PoaManager upgrade authorization + // ══════════════════════════════════════════════════════════════════════ + + function testPoaManagerUpgradeOnlyOwner() public { + ImplementationRegistry reg = new ImplementationRegistry(); + // Initialize via proxy to bypass _disableInitializers on impl + UpgradeableBeacon regBeacon = new UpgradeableBeacon(address(reg), address(this)); + BeaconProxy regProxy = new BeaconProxy(address(regBeacon), ""); + ImplementationRegistry(address(regProxy)).initialize(address(this)); + + PoaManager pm = new PoaManager(address(regProxy)); + ImplementationRegistry(address(regProxy)).transferOwnership(address(pm)); + + // Deploy a real implementation to upgrade to + DummyImplV1 implV1 = new DummyImplV1(); + DummyImplV2 implV2 = new DummyImplV2(); + + pm.addContractType("TestType", address(implV1)); + + // Non-owner cannot upgrade + vm.prank(UNAUTHORIZED); + vm.expectRevert(); + pm.upgradeBeacon("TestType", address(implV2), "v2"); + + // Owner can upgrade + pm.upgradeBeacon("TestType", address(implV2), "v2"); + assertEq(pm.getCurrentImplementationById(keccak256("TestType")), address(implV2)); + } + + function testPoaManagerRejectsEOAImplementation() public { + ImplementationRegistry reg = new ImplementationRegistry(); + UpgradeableBeacon regBeacon = new UpgradeableBeacon(address(reg), address(this)); + BeaconProxy regProxy = new BeaconProxy(address(regBeacon), ""); + ImplementationRegistry(address(regProxy)).initialize(address(this)); + + PoaManager pm = new PoaManager(address(regProxy)); + ImplementationRegistry(address(regProxy)).transferOwnership(address(pm)); + + // addContractType rejects EOA + vm.expectRevert(PoaManager.ImplZero.selector); + pm.addContractType("TestType", address(0x1234)); + + // Set up a valid type first, then try upgrading to EOA + DummyImplV1 implV1 = new DummyImplV1(); + pm.addContractType("TestType", address(implV1)); + + vm.expectRevert(PoaManager.ImplZero.selector); + pm.upgradeBeacon("TestType", address(0x5678), "v2"); + } + + // ══════════════════════════════════════════════════════════════════════ + // SECTION 3: Storage preservation after beacon upgrade + // ══════════════════════════════════════════════════════════════════════ + + function testStoragePreservedAfterBeaconUpgrade() public { + // Deploy V1 implementation behind a beacon + UniversalAccountRegistry implV1 = new UniversalAccountRegistry(); + UpgradeableBeacon beacon = new UpgradeableBeacon(address(implV1), address(this)); + BeaconProxy proxy = new BeaconProxy(address(beacon), ""); + + // Initialize and set state via proxy + UniversalAccountRegistry registry = UniversalAccountRegistry(address(proxy)); + registry.initialize(address(this)); + + // Register a user + registry.registerAccount("alice"); + assertEq(registry.getUsername(address(this)), "alice"); + + // Deploy V2 and upgrade beacon + UniversalAccountRegistry implV2 = new UniversalAccountRegistry(); + beacon.upgradeTo(address(implV2)); + + // Verify state is preserved after upgrade + assertEq(registry.getUsername(address(this)), "alice"); + } + + // ══════════════════════════════════════════════════════════════════════ + // SECTION 4: SwitchableBeacon mode-switch safety + // ══════════════════════════════════════════════════════════════════════ + + function testSwitchableBeaconMirrorToStaticPreservesProxy() public { + // Set up POA global beacon with V1 + DummyImplV1 implV1 = new DummyImplV1(); + UpgradeableBeacon poaBeacon = new UpgradeableBeacon(address(implV1), address(this)); + + // Create SwitchableBeacon in Mirror mode + SwitchableBeacon switchable = + new SwitchableBeacon(address(this), address(poaBeacon), address(0), SwitchableBeacon.Mode.Mirror); + + // Verify mirror mode returns POA impl + assertEq(switchable.implementation(), address(implV1)); + + // Switch to static (pin to current) + switchable.pinToCurrent(); + assertEq(switchable.implementation(), address(implV1)); + assertTrue(!switchable.isMirrorMode()); + + // Upgrade POA beacon to V2 - static beacon should NOT follow + DummyImplV2 implV2 = new DummyImplV2(); + poaBeacon.upgradeTo(address(implV2)); + + // SwitchableBeacon still returns V1 (pinned) + assertEq(switchable.implementation(), address(implV1)); + + // Switch back to mirror - should now follow V2 + switchable.setMirror(address(poaBeacon)); + assertEq(switchable.implementation(), address(implV2)); + assertTrue(switchable.isMirrorMode()); + } + + function testSwitchableBeaconOnlyOwnerCanSwitchModes() public { + DummyImplV1 implV1 = new DummyImplV1(); + UpgradeableBeacon poaBeacon = new UpgradeableBeacon(address(implV1), address(this)); + + SwitchableBeacon switchable = + new SwitchableBeacon(address(this), address(poaBeacon), address(0), SwitchableBeacon.Mode.Mirror); + + // Non-owner cannot pin + vm.prank(UNAUTHORIZED); + vm.expectRevert(SwitchableBeacon.NotOwner.selector); + switchable.pin(address(implV1)); + + // Non-owner cannot set mirror + vm.prank(UNAUTHORIZED); + vm.expectRevert(SwitchableBeacon.NotOwner.selector); + switchable.setMirror(address(poaBeacon)); + + // Non-owner cannot pin to current + vm.prank(UNAUTHORIZED); + vm.expectRevert(SwitchableBeacon.NotOwner.selector); + switchable.pinToCurrent(); + } + + // ══════════════════════════════════════════════════════════════════════ + // SECTION 5: Proxy initialization works correctly + // ══════════════════════════════════════════════════════════════════════ + + function testProxyCanBeInitializedWhileImplBlocked() public { + // Implementation cannot be initialized (blocked by constructor) + ImplementationRegistry impl = new ImplementationRegistry(); + vm.expectRevert(Initializable.InvalidInitialization.selector); + impl.initialize(OWNER); + + // But proxy CAN be initialized through beacon + UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), address(this)); + BeaconProxy proxy = new BeaconProxy(address(beacon), ""); + ImplementationRegistry(address(proxy)).initialize(OWNER); + assertEq(ImplementationRegistry(address(proxy)).owner(), OWNER); + } + + function testProxyCannotBeInitializedTwice() public { + UniversalAccountRegistry impl = new UniversalAccountRegistry(); + UpgradeableBeacon beacon = new UpgradeableBeacon(address(impl), address(this)); + BeaconProxy proxy = new BeaconProxy(address(beacon), ""); + + // First initialization succeeds + UniversalAccountRegistry(address(proxy)).initialize(OWNER); + + // Second initialization reverts + vm.expectRevert(Initializable.InvalidInitialization.selector); + UniversalAccountRegistry(address(proxy)).initialize(address(0xBEEF)); + } +} + +// ══════════════════════════════════════════════════════════════════════ +// Mock contracts for upgrade testing +// ══════════════════════════════════════════════════════════════════════ + +contract DummyImplV1 { + uint256 public version = 1; +} + +contract DummyImplV2 { + uint256 public version = 2; +} + +contract MockEntryPoint { + mapping(address => uint256) public balances; + + function balanceOf(address account) external view returns (uint256) { + return balances[account]; + } + + function depositTo(address account) external payable { + balances[account] += msg.value; + } +}