Summary
Influx of low-priced tier memberships dilutes the quality & exclusivity of DAO Membership scheme, undermining its intended structure.
Vulnerability Details
The MembershipFactory::createNewDAOMembership allows users to create membership by providing DAO & tier configurations. It has checks in place to standardize and enforce rules to ensure that DAOMemberships conform to a certain criteria.
One known issue is, there are no proper tier price validation checks. According to One World Project, this is the intended business logic. Since pricing is directly tied to tier power & tier power has no direct use in the protocol, this is brushed off. However, this opens doors to attackers spamming the protocol with Memberships consisting of negligible tier prices to join.
The protocol can adjust the tier pricings of a DAO through updateDAOMembership, but that's susceptible to frontrunning & there's always so much that the team could do to regulate when there's too many of the Memberships out there, leading to potential issues & risks,
Market Saturation with low-priced Tier Membership; user would be inclined join DAO that has negligible tier pricing
& reap the profits that are high in comparison to what they paid for joining. Since One World Project has no control
over distribution of profits, such a membership becomes a "money mine" where cost to benefit is heavily skewed in favor of users, effectively reducing the overall attractiveness of Memberships created by other users with fair tier pricings.
Impact on protocol's revenue & sustainablility; When the tier price is negligible, the 20% fee collected would also be
negligible leading to overall reduction in income.
Reduced demand for fair-priced tier Memberships; With abundance of low-cost options, there will be little motivation for users to purchase a fairly priced membership.
Economic Imbalance: Members earn profits based on # of MembershipERC1155 shares held, irrespective of their price, so users would prefer going for high-tiers of a Membership since there are high returns on that leading to collapse of
membership hierarchy.
This leads to an environment where protocol's intended benefits & goals are not met.
See (POC & Recommendations section for greater understanding)
Impact
A Market flooded with low-cost Memberships can severely impact protocol's economic balance, user trust & overall objectives.
Proof-Of-Code
The following event is added in MembershipFactory that the test uses,
event PlatformFeePaid(uint256 platformFee);
function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
...
...
uint256 tierPrice = daos[daoMembershipAddress].tiers[tierIndex].price;
uint256 platformFees = (20 * tierPrice) / 100;
@> emit PlatformFeePaid(platformFees);
..
..
}
Add the following test in test/MembbershipFactory.test.ts in
describe("Create New DAO Membership", function () {}. Localhost (Anvil) is used for testing,
it("Malicious user puts a low price baiting users to join their Membership", async () => {
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
CurrencyManager = await ethers.getContractFactory("CurrencyManager");
currencyManager = await CurrencyManager.deploy();
await currencyManager.deployed();
MembershipERC1155 = await ethers.getContractFactory('MembershipERC1155');
const membershipImplementation = await MembershipERC1155.deploy();
await membershipImplementation.deployed();
MembershipFactory = await ethers.getContractFactory("MembershipFactory");
membershipFactory = await MembershipFactory.deploy(currencyManager.address, owner.address, "https://baseuri.com/", membershipImplementation.address);
await membershipFactory.deployed();
await currencyManager.addCurrency(testERC20.address);
let price = ethers.utils.parseEther("0.000000000000000005");
DAOConfig1 = { ensname: "testdao.eth", daoType: DAOType.GENERAL, currency: testERC20.address, maxMembers: 60, noOfTiers: 3 };
TierConfig1 = [{ price: price, amount: 10, minted: 0, power: 12 }, { price: price, amount: 10, minted: 0, power:6 }, { price: price, amount: 10, minted: 0, power: 3 }];
const tx = await membershipFactory.connect(addr2).createNewDAOMembership(DAOConfig1, TierConfig1);
await tx.wait();
const ensAddress = await membershipFactory.getENSAddress("testdao.eth");
membershipERC1155 = await MembershipERC1155.attach(ensAddress);
await testERC20.mint(addr1.address, ethers.utils.parseEther("20"));
await testERC20.connect(addr1).approve(membershipFactory.address, ethers.utils.parseEther("100"));
await expect(membershipFactory.connect(addr1).joinDAO(membershipERC1155.address, 0)).to.emit(membershipFactory, "PlatformFeePaid").withArgs(1);
})
Tools Used
Manual Review & Hardhat Testing
Recommendations
Consider implementing min/max thresholds to bound tier prices to prevent its exploitation & maintain integrity of the protocol.
+ struct TierThreshold {
+ uint256 minPrice;
+ uint256 maxPrice;
+ }
struct TierConfig {
+ // Discussed in another submission why this is necessary since it is a separate issue.
+ uint256 tierLevel; // The tier level (0~6)
uint256 amount; // The total number of tokens available for that tier
uint256 price; // The price of a token in the given currency.
uint256 power; // The voting power of the token.
uint256 minted; // The total number of tokens already minted for that tier
}
+ mapping(uint256 => TierThreshold) public tierThreshold;
- constructor(address _currencyManager, address _owpWallet, string memory _baseURI, address _membershipImplementation)
+ constructor(..., TierThreshold[] memory thresholds) {
...
+ require(thresholds.length == 7, "Provide thresholds for all tiers");
+ for (uint256 i; i < thresholds.length; i++) {
+ tierThreshold[i] = thresholds[i];
+ }
}
function createNewDAOMembership(DAOInputConfig calldata daoConfig, TierConfig[] calldata tierConfigs)
external returns (address) {
...
...
for (uint256 i = 0; i < tierConfigs.length; i++) {
+ uint256 tierLevel = tierConfigs[i].tierLevel;
+ require(tierConfigs[i].price >= thresholds[tierLevel].minPrice && tierConfigs[i].price <= thresholds[tierLevel].maxPrice);
require(tierConfigs[i].minted == 0, "Invalid tier config");
dao.tiers.push(tierConfigs[i]);
}
}
function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
...
...
+ uint256 tierLevel = daos[daoMembershipAddress].tiers[tierIndex].tierLevel;
- IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), tierIndex, 1);
+ IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), tierLevel, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, tierIndex);
}