Project

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

M1: The "MembershipFactory.upgradeTier" Function Does Not Update the "daos[daoMembershipAddress].tiers[tierIndex].minted" Variable

Summary

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.

Vulnerability Details

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.

Impact

This vulnerability allows a user to acquire more rights than intended by the protocol.

Tools Used

Foundry and manual review

Copy this code

// SPDX-License-Identifier: UNLICENSED
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);
// Deploy Currency Manager
currencyManager = new CurrencyManager();
// Deploy Membership ERC1155 implementation
membershipERC1155 = new MembershipERC1155();
// Deploy Membership Factory
membershipFactory = new MembershipFactory(
address(currencyManager), address(owner), "https://baseuri.com/", address(membershipERC1155)
);
// Deploy Test ERC20
testERC20 = new OWPERC20("OWPERC20", "OWP");
//array of TierConfig
// Initialize Tier configurations
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, // Pass the configured tierConfig array here
currency: address(testERC20),
maxMembers: 100,
noOfTiers: 3
});
DAOInputConfig memory daoInputConfig = DAOInputConfig({
ensname: "testdao.eth",
daoType: DAOType.PUBLIC,
currency: address(testERC20),
maxMembers: 100,
noOfTiers: 3
});
//Give money to alice and bob
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 {
//Initialize DAO configurations
DAOInputConfig memory daoInputConfig = DAOInputConfig({
ensname: "testdao.eth",
daoType: DAOType.SPONSORED,
currency: address(WBTC),
maxMembers: 100,
noOfTiers: 7
});
// Initialize Tier configurations
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});
// Add currency to CurrencyManager
currencyManager.addCurrency(address(WBTC));
// Create new DAO Membership
address daoAddress = membershipFactory.createNewDAOMembership(daoInputConfig, tierConfigs);
//Alice and Bob approve WBTC
vm.prank(alice);
WBTC.approve(address(membershipFactory), 1000000);
vm.prank(bob);
WBTC.approve(address(membershipFactory), 1000000);
// Alice joins DAO 4 times to have 4 tokens tier 6
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);
// Bob joins DAO 1 time to have 1 token tier 6
vm.prank(bob);
membershipFactory.joinDAO(daoAddress, 6);
// Verify that Alice have 4 toekns tier 6 and Bob 1 token tier 6
uint256 balance = MembershipERC1155(daoAddress).balanceOf(alice, 6);
assertEq(balance, 4);
uint256 balanceBob = MembershipERC1155(daoAddress).balanceOf(bob, 6);
assertEq(balanceBob, 1);
// Check that we really have 5 token minted tier 6
TierConfig memory tiersConf = membershipFactory.tiers(daoAddress)[6];
assertEq(tiersConf.minted, 5);
assertEq(tiersConf.price, 100);
assertEq(tiersConf.amount, 10);
//We initate the update of the DAO with new configuration
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});
// Update DAO Membership
membershipFactory.updateDAOMembership("testdao.eth", tierConfigsUpdate);
// Verify that we have new updated configuration for tiers 6
TierConfig memory tiersConUpdate = membershipFactory.tiers(daoAddress)[6];
assertEq(tiersConUpdate.minted, 5);
assertEq(tiersConUpdate.price, 10);
assertEq(tiersConUpdate.amount, 10);
// Upgrade twice tier to mint 2 token tier 5 and burn 4 token tier 6
vm.prank(alice);
membershipFactory.upgradeTier(daoAddress, 6);
vm.prank(alice);
membershipFactory.upgradeTier(daoAddress, 6);
//check that alice have 2 token tier 5 and 0 token tier 6
balance = MembershipERC1155(daoAddress).balanceOf(alice, 6);
assertEq(balance, 0);
balance = MembershipERC1155(daoAddress).balanceOf(alice, 5);
assertEq(balance, 2);
//bob still have 1 token tier 6
balanceBob = MembershipERC1155(daoAddress).balanceOf(bob, 6);
assertEq(balanceBob, 1);
// Alice will join 10 time the DAO to have 10 token tier 5
for (uint256 i = 0; i < 10; i++) {
vm.prank(alice);
membershipFactory.joinDAO(daoAddress, 5);
}
// Check that the number of token mint for tier 5 is 12 , but we have 10 instead of 12
//daos[daoMembershipAddress].tiers[tierIndex].minted is not update when we upgradeTier
TierConfig memory tiersConfUpgrade = membershipFactory.tiers(daoAddress)[5];
assertEq(tiersConfUpgrade.minted, 12);
}
}

Run this code

forge test --mc Finding_01Test --mt testUpdateDAOMembershipBug

We got the result

Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 6.14s (1.43s CPU time)
Ran 1 test suite in 6.15s (6.14s CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/Finding_01Test.t.sol:Finding_01Test
[FAIL: assertion failed: 10 != 12] testUpdateDAOMembershipBug() (gas: 3095762)

Recommendations

Update the function 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.");
+ daos[daoMembershipAddress].tiers[fromTierIndex-1].minted += 1;
+ daos[daoMembershipAddress].tiers[fromTierIndex].minted -= 2;
IMembershipERC1155(daoMembershipAddress).burn(_msgSender(), fromTierIndex, 2);
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), fromTierIndex - 1, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, fromTierIndex - 1);
}
Updates

Lead Judging Commences

0xbrivan2 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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