# Learning Guide: Custody Accounting Behind a UUPS Proxy ## The two addresses that make upgrades possible Users call the **proxy**, whose address remains stable and whose storage contains the asset address, customer ledger, liabilities, owner, and pause state. The **implementation** contains executable logic. The proxy forwards each application call with `delegatecall`: implementation code runs in the proxy's context, so `address(this)` is the proxy and reads/writes affect proxy storage. Calling the implementation directly is not equivalent and is never the supported application path. UUPS places upgrade authorization in the implementation. OpenZeppelin's proxy-context checks ensure upgrade entry points run only through a compatible proxy; `BankV1._authorizeUpgrade` then restricts authorization to the owner. This keeps the proxy small, but makes implementation correctness and storage compatibility critical. ## Initializers replace constructor state A normal implementation constructor changes only the implementation's own storage. It cannot initialize the proxy storage used by delegated calls. `initialize(asset, initialOwner)` therefore performs the one-time proxy setup and initializes ownership and pausing. The encoded initializer runs atomically when the proxy is created, avoiding an uninitialized-proxy takeover window. The `BankV1` constructor calls `_disableInitializers()`. That locks the standalone implementation so an outsider cannot initialize it and create a misleading or dangerous separately owned instance. The narrow constructor annotation tells the OpenZeppelin validator why this constructor is intentional; it does not bypass storage-layout or UUPS compatibility checks. Double initialization of the proxy and direct initialization of the implementation both revert with `InvalidInitialization`. ## Storage layout is an API Delegate calls interpret numbered storage slots according to the current implementation. A compatible future implementation preserves every existing declaration in its original order and consumes reserved gap space only when new state is truly required. Safe conceptual extension: ```solidity IERC20 internal _asset; // unchanged slot mapping(address => uint256) internal _balances; // unchanged slot uint256 internal _totalLiabilities; // unchanged slot uint256 internal _newValue; // consumes reserved space uint256[46] private __gap; ``` Unsafe conceptual extension: ```solidity uint256 internal _totalLiabilities; // reordered: corrupts interpretation IERC20 internal _asset; mapping(address => uint256) internal _balances; ``` Changing order, type, inheritance order, or removing state can make balances appear as addresses or overwrite control data. A bad implementation can also remove working upgrade machinery and permanently brick future upgrades. Layout validation is necessary, but authorization and implementation behavior still require review. ## Reserves, liabilities, and surplus **Reserves** are `MockUSDC.balanceOf(proxy)`: tokens actually held by the proxy. **Liabilities** are `totalLiabilities()`: the sum the ledger owes customers. **Surplus** is reserves minus liabilities. The solvency rule is: ```text reserves >= total liabilities ``` Equality holds in the prepared Act 1 state. Anyone can transfer mock tokens directly to the proxy without receiving ledger credit, so a surplus is possible and the invariant deliberately uses `>=`. ## Exact V1 money flows For `deposit(amount)`, the bank rejects zero, paused, or reentrant calls; reads reserves; uses `SafeERC20.safeTransferFrom`; measures the exact received delta; and only then credits the sender's internal balance and total liabilities. A fee-on-transfer or otherwise unexpected asset delta reverts the entire transaction. For `withdraw(amount)`, the bank rejects zero, paused, reentrant, or underfunded ledger calls. It debits the customer's internal balance and total liabilities before `SafeERC20.safeTransfer` sends tokens. That ordering is checks-effects-interactions: validate first, commit internal effects second, interact externally last. `nonReentrant` adds a second boundary against a malicious token callback. A revert from the token rolls the whole transaction back. `SafeERC20` handles ERC-20 implementations that return `false`, omit return values, or revert in different ways. Pausing gives the owner an emergency stop for deposits and withdrawals while views remain available. There is intentionally no owner reserve sweep. An internal V2 customer transfer, when implemented during the presentation, moves ledger balances only. It must not move ERC-20 reserves or change aggregate liabilities. ## The owner is a central trust assumption The owner may pause customer actions and authorize an implementation containing arbitrary future logic. Tests proving today's V1 behavior cannot constrain tomorrow's authorized implementation. Multisig/timelocked governance and operational controls are absent from this educational V1. > **Trust boundary:** MockUSDC has no value. These contracts are educational and unaudited; real deposits must never be sent here. The owner can pause customer actions and install arbitrary future logic. UUPS mistakes can corrupt state or permanently brick upgradeability. A real custody product requires professional audits, operational key controls, multisig or timelocked governance, incident procedures, legal advice, and jurisdiction-specific compliance work. ## Local exercises: observe the named failures These commands run deterministic tests and do not require a wallet or public RPC. Add `-vvvv` to inspect a revert trace. ```bash # BankV1.InvalidAsset forge test --match-test testInitializationChecksZeroAssetBeforeZeroOwner -vv # BankV1.ZeroAmount on both customer paths forge test --match-test 'test(Deposit|Withdraw)RejectsZeroAmount' -vv # BankV1.InsufficientBalance with available/requested values forge test --match-test testWithdrawReportsAvailableAndRequestedOnInsufficientInternalBalance -vv # BankV1.UnexpectedAssetDelta with a fee-taking token forge test --match-test testFeeOnTransferDepositRevertsAndRollsBackTokenAndAccounting -vv # OpenZeppelin EnforcedPause forge test --match-test 'test(Deposit|Withdraw)RejectsCallsWhilePaused' -vv # OpenZeppelin OwnableUnauthorizedAccount forge test --match-test 'testNonOwnerCannot(Pause|Unpause|AuthorizeUpgrade)' -vv # OpenZeppelin InvalidInitialization forge test --match-test 'test(ProxyCannotBeInitializedTwice|ImplementationCannotBeInitialized)' -vv # OpenZeppelin ReentrancyGuardReentrantCall forge test --match-test testDepositPropagatesNestedRevertAtomicallyWhenConfigured -vv # DemoScript.UnsupportedChain (the test deliberately uses a rejected chain) forge test --match-test testUnsupportedChainsAreRejectedBeforeBroadcast -vv # DemoScript.ManifestChainMismatch and DemoScript.MissingCode forge test --match-test 'test(WrongManifestChain|AddressWithoutCode)IsRejected' -vv # DemoScript.InvalidDeploymentBlock forge test --match-test testActiveManifestRejectsDeploymentBlockZero -vv # DemoScript.InvalidManifestSchema forge test --match-test testLegacyParallelActorManifestIsRejected -vv # DemoScript.InvalidActorConfiguration forge test --match-test testAnvilManifestRequiresOwnerAtActorZero -vv # DemoScript.UnexpectedState, including label and expected/actual values forge test --match-test testUnexpectedStateExerciseRevertsThroughAssertionBranch -vv # DemoScript.UnexpectedAddress, including expected/actual implementation addresses forge test --match-test testCheckStateRejectsManifestImplementationMismatch -vv # CheckState.Insolvent with reserves below ledger liabilities forge test --match-test testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches -vv # CheckState.UnknownStage through the real state checker forge test --match-test testCheckStateExercisesRevertThroughInsolventAndUnknownStageBranches -vv ``` Finish by running `make verify`; it combines unit, fuzz, invariant, script, process-safety, scanner, and web gates.