#21DaysSolidityChallenge Day 10: Elevate Your Smart Contracts with Upgradeability — Implementing Proxy Patterns 🚀🔗
🚀 Buy me a coffee! ☕ http://buymeacoffee.com/solidity
👋 Welcome to Day 10 of our Solidity Code Challenge series! Today, we’re diving into the fascinating world of upgradeable smart contracts. Imagine having the ability to upgrade your deployed smart contracts without disrupting their functionality. This is where proxy patterns come into play. You’ll learn about proxy contracts and implementation contracts and then proceed to upgrade your earlier token contract (Day 3) to make it upgradeable.
Oh, this magical link is just sooo tempting! 🪄✨ Click away, my dear friend. 😉
🔗 Upgradeable Contracts: Quick Overview
Before we embark on our upgradeable contract journey, let’s understand why upgradeability is crucial in the world of blockchain and what proxy patterns are:
- Upgradeable Contracts: In blockchain development, upgrading a contract often requires deploying a new one, which can be cumbersome and risky. Upgradeable contracts provide a way to modify the contract’s logic while preserving its state and history.
- Proxy Patterns: Proxy patterns involve the use of proxy contracts that act as intermediaries between users and implementation contracts. Users interact with the proxy, which delegates calls to the implementation contract, enabling seamless upgrades.
Now, let’s explore the magic of upgradeable contracts!
Step 1: Setting Up Your Development Environment
Before we begin, ensure you have the following tools and accounts ready:
1. Ethereum Wallet: You’ll need an Ethereum wallet like MetaMask to interact with the Ethereum blockchain.
2. Solidity Compiler: Have the Solidity compiler (solc) installed on your computer or use online Solidity development environments like Remix.
3. Test Network: Choose a test network (e.g., Ropsten, Rinkeby, or Kovan) to deploy and test your upgradeable token contract without using real Ether.
4. Integrated Development Environment (IDE): Consider using an IDE like Visual Studio Code with Solidity extensions for a smoother coding experience.
Step 2: Understanding Proxy Patterns
Proxy patterns involve two main components:
- Proxy Contract: This contract is the one that users interact with. It delegates function calls to the implementation contract.
- Implementation Contract: This contract contains the logic of your smart contract. It can be upgraded without changing the proxy contract.
Here’s a simplified example of how proxy patterns work:
// Proxy contract
contract Proxy {
address public implementation;
constructor(address _implementation) {
implementation = _implementation;
}
function upgrade(address _newImplementation) external {
require(msg.sender == owner, "Only the owner can upgrade");
implementation = _newImplementation;
}
// Delegate all calls to the implementation contract
fallback() external {
address _impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
}
// Implementation contract
contract MyToken {
mapping(address => uint256) public balances;
uint256 public totalSupply;
constructor(uint256 _initialSupply) {
balances[msg.sender] = _initialSupply;
totalSupply = _initialSupply;
}
function transfer(address _to, uint256 _value) external {
// Transfer logic
}
}
In this example:
- The `Proxy` contract stores the address of the `Implementation` contract.
- The `upgrade` function allows the contract owner to update the implementation contract’s address.
- The `fallback` function delegates calls to the implementation contract using assembly code.
Step 3: Upgrading Your Token Contract
Now that you understand proxy patterns, let’s upgrade your token contract from Day 3 to make it upgradeable. We’ll create a proxy contract that interacts with the implementation contract of your token.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// Interface for the ERC-20 token methods
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// Implementation contract for the ERC-20 token
contract TokenImplementation is IERC20 {
string public name;
string public symbol;
uint8 public decimals;
uint256 private _totalSupply;
address private _owner;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
decimals = _decimals;
_totalSupply = _initialSupply * 10 ** uint256(decimals);
_owner = msg.sender;
_balances[msg.sender] = _totalSupply;
}
function totalSupply() external view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) external view override returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint256 amount) external override returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}
function allowance(address owner, address spender) external view override returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) external override returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
_transfer(sender, recipient, amount);
_approve(sender, msg.sender, _allowances[sender][msg.sender] - amount);
return true;
}
function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {
_approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue);
return true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {
_approve(msg.sender, spender, _allowances[msg.sender][spender] - subtractedValue);
return true;
}
function _transfer(address sender, address recipient, uint256 amount) internal {
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance");
_balances[sender] -= amount;
_balances[recipient
] += amount;
emit Transfer(sender, recipient, amount);
}
function _approve(address owner, address spender, uint256 amount) internal {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
// Additional functions can be added to enhance the token's functionality
}
// Proxy contract for the upgradeable ERC-20 token
contract TokenProxy is IERC20 {
address public implementation;
address private _owner;
constructor(address _implementation) {
implementation = _implementation;
_owner = msg.sender;
}
function upgrade(address _newImplementation) external {
require(msg.sender == _owner, "Only the owner can upgrade");
implementation = _newImplementation;
}
fallback() external {
address _impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
function totalSupply() external view override returns (uint256) {
bytes memory payload = abi.encodeWithSignature("totalSupply()");
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (uint256));
}
function balanceOf(address account) external view override returns (uint256) {
bytes memory payload = abi.encodeWithSignature("balanceOf(address)", account);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (uint256));
}
function transfer(address recipient, uint256 amount) external override returns (bool) {
bytes memory payload = abi.encodeWithSignature("transfer(address,uint256)", recipient, amount);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (bool));
}
function allowance(address owner, address spender) external view override returns (uint256) {
bytes memory payload = abi.encodeWithSignature("allowance(address,address)", owner, spender);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (uint256));
}
function approve(address spender, uint256 amount) external override returns (bool) {
bytes memory payload = abi.encodeWithSignature("approve(address,uint256)", spender, amount);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (bool));
}
function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
bytes memory payload = abi.encodeWithSignature("transferFrom(address,address,uint256)", sender, recipient, amount);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (bool));
}
function increaseAllowance(address spender, uint256 addedValue) external returns (bool) {
bytes memory payload = abi.encodeWithSignature("increaseAllowance(address,uint256)", spender, addedValue);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (bool));
}
function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool) {
bytes memory payload = abi.encodeWithSignature("decreaseAllowance(address,uint256)", spender, subtractedValue);
(bool success, bytes memory data) = implementation.delegatecall(payload);
require(success, "Delegatecall failed");
return abi.decode(data, (bool));
}
// Additional functions can be added to enhance the token's functionality
}
In this upgraded token contract:
- We’ve split the contract into two parts: `TokenImplementation` contains the token logic, while `TokenProxy` is the proxy contract that delegates calls to the implementation.
- The `upgrade` function in the proxy contract allows the contract owner to update the implementation address.
- The `fallback` function in the proxy contract delegates calls to the implementation contract using assembly code.
- For each function in the ERC-20 interface, we added a corresponding function in the proxy contract that forwards the call to the implementation contract.
Step 4: Compiling the Upgradeable Token Contract
Compile your upgradeable token contract using the Solidity compiler. Use the following command in your terminal:
solc - bin - abi TokenImplementation.sol
solc - bin - abi TokenProxy.sol
This command generates the bytecode and ABI for both the `TokenImplementation` and `TokenProxy` contracts.
Step 5: Deploying the Upgradeable Token Contract
Deploy your upgradeable token contract to a test network. Follow these steps:
1. Open your Ethereum wallet (e.g., MetaMask) and switch to the Ropsten network.
2. Acquire some test Ether for Ropsten from a faucet if needed.
3. Deploy the `TokenImplementation` contract first. In Remix or a similar tool:
- Switch to the “Deploy & Run Transactions” tab.
- Ensure your environment is set to “Injected Web3” (if you’re using MetaMask).
- Deploy the `TokenImplementation` contract with the desired parameters (name, symbol, decimals, initial supply).
4. After deploying `TokenImplementation`, note its address.
5. Deploy the `TokenProxy` contract, passing the address of the deployed `TokenImplementation` as a constructor parameter.
6. Confirm the deployment in your wallet, and your upgradeable token contract is now live on the Ropsten network.
Step 6: Interacting with the Upgradeable Token Contract
Now that your upgradeable token contract is deployed, you can interact with it just like any other ERC-20 token. You can use Remix, Truffle, or your preferred Ethereum wallet for this.
1. Load the `TokenProxy` contract in Remix, and you will see the ERC-20 functions available for interaction.
2. Perform transfers, approve spending allowances, and check balances to ensure the token operates as expected.
Step 7: Upgrading the Implementation Contract
Let’s test the upgradeability of your contract by deploying a new version of the `TokenImplementation` contract and upgrading the proxy contract to use the new implementation.
1. Create a new version of the `TokenImplementation` contract with any desired changes (e.g., add new functions or modify existing ones).
2. Deploy the new `TokenImplementation` contract on the same test network, just like you did in Step 5.
3. Call the `upgrade` function of the `TokenProxy` contract, passing the address of the new `TokenImplementation` contract as the upgrade target.
4. Confirm the upgrade by interacting with the proxy contract, and the new implementation’s logic will be in effect.
Step 8: Writing Tests for the Upgradeable Token Contract
To ensure that your upgradeable token contract functions correctly, let’s write tests. You can use a testing framework like Truffle or Hardhat. Here’s a simplified example using Truffle:
// In a Truffle test file, e.g., TokenUpgrade.test.js
const TokenProxy = artifacts.require("TokenProxy");
const TokenImplementation = artifacts.require("TokenImplementation");
contract("TokenUpgrade", (accounts) => {
it("should upgrade and retain balances", async () => {
const owner = accounts[0];
const newOwner = accounts[1];
const initialSupply = 10000;
const newTotalSupply = 20000;
// Deploy the initial TokenImplementation
const initialImpl = await TokenImplementation.new("MyToken", "MTK", 18, initialSupply);
const proxy = await TokenProxy.new(initialImpl.address);
// Transfer ownership to a new account
await proxy.transferOwnership(newOwner, { from: owner });
// Deploy a new version of TokenImplementation
const newImpl = await TokenImplementation.new("UpgradedToken", "UTK", 18, newTotalSupply);
// Upgrade the proxy contract
await proxy.upgrade(newImpl.address, { from: newOwner });
// Check the total supply after upgrade
const totalSupply = await proxy.totalSupply();
assert.equal(totalSupply, newTotalSupply, "Total supply should be upgraded");
// Transfer tokens and verify balances
const sender = owner;
const recipient = accounts[2];
const amount = 1000;
await proxy.transfer(recipient, amount, { from: sender });
const senderBalance = await proxy.balanceOf(sender);
const recipientBalance = await proxy.balanceOf(recipient);
assert.equal(senderBalance.toNumber(), initialSupply - amount, "Sender balance should be reduced");
assert.equal(recipientBalance.toNumber(), amount, "Recipient balance should be increased");
});
});
This Truffle test checks the upgradeability of the token contract and ensures that balances are retained after an upgrade.
Step 9: Running the Tests
Execute the tests using the Truffle framework. In your project directory, run:
truffle test
This command will run your tests and verify that the upgradeable token contract works as expected.
Conclusion 🚀🌟
Congratulations! You’ve unlocked the power of upgradeable smart contracts using proxy patterns. In Day 10, you learned how to split your smart contract into an implementation contract and a proxy contract, enabling seamless upgrades without disrupting your users’ experience. This advanced technique is crucial in the world of blockchain, where adaptability and scalability are paramount.
As you continue your Solidity journey, remember that upgradeable contracts are a powerful tool, but they also require careful planning and testing to ensure they function correctly. Keep exploring, experimenting, and building. You’re on your way to becoming a Solidity superstar! 🌟
Stay tuned for more exciting challenges and concepts in our Solidity Code Challenge series. Happy coding! 🚀🔗