Hi,
I’m trying to understand how Upgradeable contract using Proxy pattern works. From my understanding, upgradeable contract using proxy pattern splits regular contract into three separated parts: Proxy, Implementation and State. When upgrading, only new implementation is upgraded whereas its state is unchanged. Proxy contract will ‘delegate call’ to a new implementation whenever a contract’s function is called. However, this behavior is not correct when I test some basic contracts using truffle and OpenZeppelin Upgrades Plugins. In case 1 below, old implementation can also modify state variables while it is failed to do so in case 2. Would you please explain why it has this different behavior?
Code to reproduce
Environment:
Truffle v5.3.0 (core: 5.3.0)
Solidity v0.5.16 (solc-js)
Node v15.12.0
Web3.js v1.2.9
- Case 1: ‘Upgrade2’ contract has all functions of ‘Upgrade1’ contract and has an additional function ‘getBalanceOf()’
pragma solidity >=0.5.0 <0.8.0;
contract Upgrade1 {
mapping(address => uint) balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function getBalance() external view returns (uint) {
return address(this).balance;
}
}
contract Upgrade2 {
mapping(address => uint) balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function getBalance() external view returns (uint) {
return address(this).balance;
}
function getBalanceOf(address _acc) external view returns (uint) {
return balances[_acc];
}
}
- Case 2: A function ‘getBalance()’ is modified in ‘Upgrade4’ contract
pragma solidity >=0.5.0 <0.8.0;
contract Upgrade3 {
mapping(address => uint) balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function getBalance() external view returns (uint) {
return address(this).balance;
}
}
contract Upgrade4 {
mapping(address => uint) balances;
function deposit() external payable {
balances[msg.sender] = msg.value;
}
function getBalance(address _addr) external view returns (uint) {
return balances[_addr];
}
}
Migrations:
const {deployProxy, upgradeProxy} = require('@openzeppelin/truffle-upgrades');
const Upgrade1 = artifacts.require('Upgrade1');
const Upgrade2 = artifacts.require("Upgrade2");
const Upgrade3 = artifacts.require('Upgrade3');
const Upgrade4 = artifacts.require("Upgrade4");
module.exports = async function(deployer) {
var upgrade1 = await deployProxy(Upgrade1, {deployer});
await upgradeProxy(upgrade1.address, Upgrade2, {deployer});
var upgrade3 = await deployProxy(Upgrade3, {deployer});
await upgradeProxy(upgrade3.address, Upgrade4, {deployer});
}
Test:
const { deployProxy, upgradeProxy } = require("@openzeppelin/truffle-upgrades");
const Upgrade1 = artifacts.require("Upgrade1");
const Upgrade2 = artifacts.require("Upgrade2");
const Upgrade3 = artifacts.require('Upgrade3');
const Upgrade4 = artifacts.require("Upgrade4");
contract('Upgradeable contracts test', () => {
let up1, up2, up3, up4, accounts;
before(async () => {
up1 = await deployProxy(Upgrade1);
up2 = await upgradeProxy(up1.address, Upgrade2);
up3 = await deployProxy(Upgrade3);
up4 = await upgradeProxy(up3.address, Upgrade4);
accounts = await web3.eth.getAccounts();
});
it('Contracts', async () => {
console.log("Contract 1: ", up1.address);
console.log("Contract 2: ", up2.address);
console.log("Contract 3: ", up3.address);
console.log("Contract 4: ", up4.address);
})
it('Deposit to Upgrade1 Contract', async() => {
await up1.deposit({from: accounts[1], value: 1000});
await up1.deposit({from: accounts[2], value: 5000});
var balanceCont = await up1.getBalance();
console.log('Contract Balance: ', web3.utils.BN(balanceCont).toNumber());
});
it('Upgrade2 Contract - Success', async() => {
await up2.deposit({from: accounts[1], value: 1000});
await up2.deposit({from: accounts[2], value: 5000});
var balanceCont = await up2.getBalance();
console.log('Contract Balance: ', web3.utils.BN(balanceCont).toNumber());
});
it('Deposit to Upgrade3 Contract', async() => {
await up3.deposit({from: accounts[1], value: 1000});
await up3.deposit({from: accounts[2], value: 5000});
var balanceCont = await up3.getBalance();
console.log('Contract Balance: ', web3.utils.BN(balanceCont).toNumber());
});
it('Upgrade4 Contract - Success', async() => {
await up4.deposit({from: accounts[1], value: 1000});
await up4.deposit({from: accounts[2], value: 5000});
var balanceCont = await up4.getBalance(accounts[2]);
console.log('Contract Balance: ', web3.utils.BN(balanceCont).toNumber());
});
});
Results:
Contract: Upgradeable contracts test
Contract 1: 0x7146590Cc1354f48E2ab8bf09b29e0fE68454EcD
Contract 2: 0x7146590Cc1354f48E2ab8bf09b29e0fE68454EcD
Contract 3: 0x6C258784467Ab08f4563512412F18842637a2c5e
Contract 4: 0x6C258784467Ab08f4563512412F18842637a2c5e
✓ Contracts
Contract Balance: 6000
✓ Deposit to Upgrade1 Contract (252ms)
Contract Balance: 12000
✓ Upgrade2 Contract - Success (236ms)
1) Deposit to Upgrade3 Contract
> No events were emitted
Contract Balance: 5000
✓ Upgrade4 Contract - Success (248ms)
4 passing (4s)
1 failing
1) Contract: Upgradeable contracts test
Deposit to Upgrade3 Contract:
Error: Returned error: VM Exception while processing transaction: revert
at Context.<anonymous> (test/2_Upgrade.js:42:37)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:94:5)