So you want an “upgradeable smart account.” What does that mean? Let’s walk through the 4337 path and 7702 path in detail.

The concepts involved can be tricky because they are very similar in some places, use overloaded terms like “upgrade,” and overlap a bit. Let’s start at the beginning and build our understanding from the ground up.

4337 Smart Account

So you deployed an “upgradeable” smart account. How does it all work? We’ll use Coinbase Smart Wallet as an example, but the principles are the same for other smart accounts.

I created a CBSW with the address 0x3f56e996934c952a074672ffdbe51042e046c80a. Here it is on Basescan.

The proxy

The code deployed at this address is an ERC-1967 UUPS proxy. Let’s break this down.

It’s a proxy because it delegates all incoming calls to a separate implementation contract. We’ll call these two contracts the “proxy contract” and “implementation contract.” Specifically, it uses the DELEGATECALL opcode to make calls that use the storage from my smart account’s address 0x3f56…c80a but the contract bytecode deployed at a different implementation contract address.

It’s an ERC-1967 proxy because it stores the implementation contract address in a standard storage slot, bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1). This convention helps tools like block explorers look at a proxy and figure out the address where its implementation contract lives. Choosing a slot by hashing the string 'eip1967.proxy.implementation' produces a deterministic magic value that will never be generated by the Solidity compiler’s storage layout algorithm. This prevents accidental storage conflicts that accidentally overwrite storage values.

It’s a UUPS proxy because its upgrade function exists on the implementation contract and not the proxy contract. The authorization rules and logic for changing the proxy’s implementation contract are defined by the implementation, and not directly in the proxy contract. So there is no owner address or upgrade function defined on the proxy itself.

How the proxy works

Bear with me, but let’s go one step deeper and look at the actual code of this account:

$ cast code 0x3f56e996934C952a074672fFDbe51042e046c80a \\
  --rpc-url "<https://mainnet.base.org>"                                        
0x363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3

This is short enough that we can decompile the contract bytecode ourselves. The whole contract is 25 opcodes plus data:

// Copy calldata into memory and push offsets onto the stack
36 CALLDATASIZE
3d RETURNDATASIZE
3d RETURNDATASIZE
37 CALLDATACOPY
3d RETURNDATASIZE
3d RETURNDATASIZE
36 CALLDATASIZE
3d RETURNDATASIZE

// Load the implementation address from storage and push it onto the stack. 
// This 32 byte value is the implementation storage slot:
// keccak256('eip1967.proxy.implementation')) - 1
7f PUSH32 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
54 SLOAD

// Get the amount of gas provided and push it onto the stack
5a GAS

// Perform the delegatecall
// Takes all the values off the stack and pushes 1 for success or 0 for failure
f4 DELEGATECALL

// Check the result.
// If the call succeeded, return the return data.
// If the call failed, return the revert data.
3d RETURNDATASIZE
60 PUSH1 0x00
80 DUP1
3e RETURNDATACOPY
60 PUSH1 0x38
57 JUMPI
3d RETURNDATASIZE
60 PUSH1 0x00
fd REVERT
5b JUMPDEST
3d RETURNDATASIZE
60 PUSH1 0x00
f3 RETURN

That’s it! Notice that there is no ownership, auth, or upgrade logic in this extremely minimal proxy code. All it does is 1) look up an address from a known storage slot following a standard convention and 2) delegatecall into that address.

How upgrades work

So if this proxy has no upgrade logic, how does an actual upgrade work? In a UUPS proxy, logic related to upgrades lives on the implementation, which must implement an interface like this:

interface UUPSUpgradeable {

  // Must return the storage slot where the proxy stores its implementation 
  // address. Once again, this is the magic EIP-1967 value:
  // keccak256('eip1967.proxy.implementation')) - 1
  function proxiableUUID() public view returns (bytes32);
  
  // Implements authorization logic to check whether the msg.sender
  // is allowed to upgrade the proxy.
  function _authorizeUpgrade(address newImplementation) internal;
 
  // Checks that the call is coming from a proxy.
  // Checks that the upgrade is authorized by calling _authorizeUpgrade.
  // Gets the storage slot to overwrite from proxiableUUID.
  // Checks that the new implementation also implements proxiableUUID.
  // Overwrites the implementation address in storage with a new address. 
  // Calls the new implementation with the provided calldata.
  function upgradeToAndCall(
    address newImplementation, 
    bytes calldata data
  ) public payable;  

}