Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: high
Valid

MembershipERC1155 proxy cannot be upgraded

Summary

MembershipERC1155 proxy cannot be upgraded due to incorrect execution flow.

Vulnerability Details

When a DAO membership is created by MembershipFactory, it is deployed as a MembershipERC1155 proxy, and proxyAdmin is used.

MembershipFactory.sol#L72-L76:

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
membershipImplementation,
@> address(proxyAdmin),
abi.encodeWithSignature("initialize(string,string,string,address,address)", daoConfig.ensname, "OWP", baseURI, _msgSender(), daoConfig.currency)
);

The deployed MembershipERC1155 proxy is expected to be upgraded by proxyAdmin by calling upgradeAndCall().

ProxyAdmin.sol#L38-L44:

function upgradeAndCall(
ITransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}

The expected upgrade flow would be like:

upgradeAndCall() upgradeToAndCall()

admin --------------------> proxyAdmin --------------------> proxy

Unfortunately, this simply won't work.

When a TransparentUpgradeableProxy instance is called, the call will be forwarded to _fallback() function, and only if the caller is the admin of the proxy, the proxy is upgraded.

TransparentUpgradeableProxy.sol#L95-L105:

function _fallback() internal virtual override {
@> if (msg.sender == _proxyAdmin()) {
if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
revert ProxyDeniedAdminAccess();
} else {
@> _dispatchUpgradeToAndCall();
}
} else {
super._fallback();
}
}

TransparentUpgradeableProxy.sol#L88-L90:

function _proxyAdmin() internal view virtual returns (address) {
return _admin;
}

The problem is proxyAdmin is not the admin of the MembershipERC1155 proxy, as can be seen in the constructor of TransparentUpgradeableProxy, when a proxy instance is deployed, it creates an an associated ProxyAdmin instance (let's call it proxyAdmin2) to manage the proxy, so the admin of the MembershipERC1155 proxy is proxyAdmin2 instead of proxyAdmin.

TransparentUpgradeableProxy.sol#L79-L83:

constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
@> _admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}

As a result, any call to upgrade a MembershipERC1155 proxy will not be process internally but forwarded to the implementation, and the call will revert.

Therefore, to make MembershipERC1155 proxy actually be upgraded, the flow should be like:

upgradeAndCall() upgradeAndCall() upgradeToAndCall()

admin --------------------> proxyAdmin --------------------> proxyAdmin2 --------------------> proxy

However, we cannot make proxyAdmin call upgradeAndCall() in proxyAdmin2, nor can we transfer proxyAdmin2 ownership to admin.

Please follow the steps to run PoC to verify:

  1. Follow the instructions to add foundry to the project (You may need to change hardhat version to 2.17.2 in package.json);

  2. Create a test file (.t.sol) containing below in /test directory;

  3. Run forge test --mt testAudit_upgradeProxy.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;
import {Test} from "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/dao/MembershipFactory.sol";
import "../contracts/dao/CurrencyManager.sol";
import "../contracts/dao/tokens/MembershipERC1155.sol";
import {DAOType, DAOConfig, DAOInputConfig, TierConfig} from "../contracts/dao/libraries/MembershipDAOStructs.sol";
contract Audit is Test {
address admin = makeAddr("Admin");
address owpWallet = makeAddr("owpWallet");
ERC20 WETH = new MockERC20("Wrapped ETH", "WETH", 18);
ERC20 WBTC = new MockERC20("Wrapped BTC", "WBTC", 8);
ERC20 USDC = new MockERC20("USDC", "USDC", 6);
MembershipFactory membershipFactory;
CurrencyManager currencyManager;
function setUp() public {
vm.startPrank(admin);
// Deploy CurrencyManager
currencyManager = new CurrencyManager();
currencyManager.addCurrency(address(WETH));
currencyManager.addCurrency(address(WBTC));
currencyManager.addCurrency(address(USDC));
// Deploy MembershipERC1155
MembershipERC1155 membershipERC1155Implementation = new MembershipERC1155();
// Deploy MembershipFactory
membershipFactory = new MembershipFactory(
address(currencyManager),
owpWallet,
"https://baseuri.com/",
address(membershipERC1155Implementation)
);
vm.stopPrank();
}
function testAudit_upgradeProxy() public {
// Create DAO
address creator = makeAddr("Creator");
DAOInputConfig memory daoInputConfig = DAOInputConfig({
ensname: "SPONSORED DAO",
daoType: DAOType.SPONSORED,
currency: address(USDC),
maxMembers: 127,
noOfTiers: 7
});
vm.startPrank(creator);
address daoMemebershipProxy = membershipFactory.createNewDAOMembership(
daoInputConfig,
createTierConfigs(
daoInputConfig.noOfTiers,
ERC20(daoInputConfig.currency).decimals()
)
);
vm.stopPrank();
// Deploy new MembershipERC1155 implementation
MembershipERC1155v2 membershipERC1155v2Implementation = new MembershipERC1155v2();
vm.startPrank(admin);
membershipFactory.updateMembershipImplementation(address(membershipERC1155v2Implementation));
ProxyAdmin proxyAdmin = membershipFactory.proxyAdmin();
// Upgrade MembershipERC1155 will revert
vm.expectRevert();
proxyAdmin.upgradeAndCall(ITransparentUpgradeableProxy(daoMemebershipProxy), address(membershipERC1155v2Implementation), "");
vm.stopPrank();
}
function createTierConfigs(uint noOfTiers, uint8 decimals) private returns (TierConfig[] memory tiers) {
tiers = new TierConfig[](noOfTiers);
uint price = 1 * 10 ** decimals;
uint power = 1;
for (int i = int(noOfTiers) - 1; i >= 0; --i) {
uint index = uint(i);
tiers[index] = TierConfig({
amount: 2 ** index,
price: price,
power: power,
minted: 0
});
price *= 2;
power *= 2;
}
}
}
contract MockERC20 is ERC20 {
uint8 _decimals;
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_
) ERC20(name_, symbol_) {
_decimals = decimals_;
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}
contract MembershipERC1155v2 is MembershipERC1155 {}

Impact

MembershipERC1155 proxy cannot be upgraded.

Tools Used

Manual Review

Recommendations

There are 2 mitigations:

  1. The simplest solution is to declare proxyAdmin as an address (EOA/Multisig) instead of an ProxyAdmin instance, then it can call proxyAdmin2 (the address can be accessed by reading the proxy's ERC-1967 admin slot) to upgrade MembershipERC1155proxy.

  2. Or we can override TransparentUpgradeableProxy's _proxyAdmin() in a child contract, and use the child contract to create DAO membership proxy.

contract MembershipERC1155Proxy is TransparentUpgradeableProxy {
constructor(
address _logic,
address initialOwner,
bytes memory _data
) TransparentUpgradeableProxy(_logic, initialOwner, _data) {}
function _proxyAdmin() internal view override returns (address) {
return ProxyAdmin(super._proxyAdmin()).owner();
}
}

MembershipFactory.sol#L72-L76:

- TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
+ TransparentUpgradeableProxy proxy = new MembershipERC1155Proxy(
membershipImplementation,
address(proxyAdmin),
abi.encodeWithSignature("initialize(string,string,string,address,address)", daoConfig.ensname, "OWP", baseURI, _msgSender(), daoConfig.currency)
);
Updates

Lead Judging Commences

0xbrivan2 Lead Judge
7 months ago
0xbrivan2 Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

MembershipERC1155 implementations can not be upgraded for already deployed proxies

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.