Deploying Upgradeable Contracts using UUPS with Foundry
UUPS (Universal Upgradeable Proxy Standard)
UUPS is an upgradeable proxy pattern that addresses some limitations of the transparent proxy pattern. Key features include:
- Upgrade Logic in Implementation: The upgrade functionality is placed in the implementation contract rather than the proxy.
 - Gas Efficiency: More gas-efficient for users as there's no need to check the caller's identity on every function call.
 - Smaller Proxy Contract: The proxy contract is simpler and smaller, potentially reducing deployment costs.
 
Comparison of Proxy Patterns
- 
UUPS vs. Transparent Proxy:
- UUPS places upgrade logic in the implementation, while transparent proxy keeps it in the proxy contract.
 - UUPS is more gas-efficient for regular function calls.
 - Transparent proxy has a larger proxy contract but simpler implementation contracts.
 
 - 
UUPS vs. Regular Upgradeable Proxy:
- UUPS provides better security against accidental contract locking.
 - Regular upgradeable proxies are simpler but may be more prone to errors during upgrades.
 
 - 
Common Features:
- All patterns allow upgrading contract logic without changing the contract address.
 - They all use delegate calls to forward function calls to the implementation contract.
 
 
UUPS is often preferred for its balance of security, gas efficiency, and flexibility. In this tutorial, we'll implement and deploy upgradeable contracts using UUPS on Conflux eSpace.
Project Setup
- Create a new Foundry project:
 
forge init uups-proxy-foundry-demo
cd uups-proxy-foundry-demo
- Install OpenZeppelin contracts:
 
forge install OpenZeppelin/openzeppelin-contracts
forge install OpenZeppelin/openzeppelin-contracts-upgradeable
- Configure Foundry
 
Update your foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.24"
remappings = [
    "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
    "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/"
]
- Create a 
.envfile: 
PRIVATE_KEY=your_private_key_here
RPC_URL=https://evmtestnet.confluxrpc.com
Writing Smart Contracts
- Create the initial Counter contract in 
src/Counter.sol: 
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract Counter is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public number;
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
    function initialize() public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        number = 0;
    }
    function setNumber(uint256 newNumber) public {
        number = newNumber;
    }
    function increment() public {
        number++;
    }
    function getCount() public view returns (uint256) {
        return number;
    }
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
- Create CounterV2 in 
src/CounterV2.sol: 
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract CounterV2 is UUPSUpgradeable, OwnableUpgradeable {
    uint256 private count;
    event CountChanged(uint256 count);
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
    function initialize() public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
    }
    function increment() public {
        count += 1;
        emit CountChanged(count);
    }
    function getCount() public view returns (uint256) {
        return count;
    }
    function reset() public {
        count = 0;
        emit CountChanged(count);
    }
    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
} 
Deployment Scripts
- Create a deployment script in 
script/DeployCounter.s.sol: 
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Script.sol";
import "../src/Counter.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract DeployCounter is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);
        // Deploy implementation
        Counter counter = new Counter();
        console.log("Implementation deployed to:", address(counter));
        // Encode initialize function call
        bytes memory data = abi.encodeWithSelector(Counter.initialize.selector);
        // Deploy proxy
        ERC1967Proxy proxy = new ERC1967Proxy(
            address(counter),
            data
        );
        console.log("Proxy deployed to:", address(proxy));
        // Verify deployment
        Counter proxiedCounter = Counter(address(proxy));
        console.log("Initial count:", proxiedCounter.getCount());
        vm.stopBroadcast();
    }
} 
- Create an upgrade script in 
script/UpgradeCounter.s.sol: 
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Script.sol";
import "../src/Counter.sol";
import "../src/CounterV2.sol";
contract UpgradeCounter is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        address proxyAddress = vm.envAddress("PROXY_ADDRESS");
        // Test before upgrade
        console.log("============ Before Upgrade ============");
        Counter counter = Counter(proxyAddress);
        uint256 valueBefore = counter.getCount();
        console.log("Current count:", valueBefore);
        vm.startBroadcast(deployerPrivateKey);
        // Deploy new implementation
        CounterV2 counterV2 = new CounterV2();
        console.log("\n============ Deploying New Implementation ============");
        console.log("New implementation:", address(counterV2));
        // Upgrade proxy to new implementation
        Counter(proxyAddress).upgradeToAndCall(
            address(counterV2),
            "" // Empty bytes string since we don't need to call any initialization function
        );
        vm.stopBroadcast();
        // Test after upgrade
        console.log("\n============ After Upgrade ============");
        CounterV2 upgradedCounter = CounterV2(proxyAddress);
        uint256 valueAfter = upgradedCounter.getCount();
        console.log("Count after upgrade:", valueAfter);
        vm.startBroadcast(deployerPrivateKey);
        upgradedCounter.increment();
        vm.stopBroadcast();
        uint256 valueAfterIncrement = upgradedCounter.getCount();
        console.log("Count after increment:", valueAfterIncrement);
        vm.startBroadcast(deployerPrivateKey);
        upgradedCounter.reset();
        vm.stopBroadcast();
        uint256 valueAfterReset = upgradedCounter.getCount();
        console.log("Count after reset:", valueAfterReset);
        // Verify upgrade results
        require(valueAfter == valueBefore, "State verification failed: Value changed during upgrade");
        require(valueAfterIncrement == valueAfter + 1, "Function verification failed: Increment not working");
        require(valueAfterReset == 0, "Function verification failed: Reset not working");
        console.log("\n============ Upgrade Successful ============");
        console.log("1. State preserved: Initial count maintained after upgrade");
        console.log("2. New functions working: Increment and Reset successfully added");
    }
}
Testing
Create a test file in test/Counter.t.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/Counter.sol";
import "../src/CounterV2.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract CounterTest is Test {
    Counter public implementation;
    CounterV2 public implementationV2;
    ERC1967Proxy public proxy;
    address owner = address(this);
    function setUp() public {
        // Deploy implementation
        implementation = new Counter();
        
        // Encode initialize function call
        bytes memory data = abi.encodeWithSelector(Counter.initialize.selector);
        
        // Deploy proxy
        proxy = new ERC1967Proxy(
            address(implementation),
            data
        );
    }
    function testIncrement() public {
        Counter(address(proxy)).increment();
        assertEq(Counter(address(proxy)).getCount(), 1);
    }
    function testUpgrade() public {
        // Deploy new implementation
        implementationV2 = new CounterV2();
        
        // Upgrade
        Counter(address(proxy)).upgradeTo(address(implementationV2));
        
        // Test new functionality
        CounterV2(address(proxy)).increment();
        assertEq(CounterV2(address(proxy)).getCount(), 1);
        
        CounterV2(address(proxy)).reset();
        assertEq(CounterV2(address(proxy)).getCount(), 0);
    }
}