Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: high
Valid

### [H-1] No contract-level verification of KYC requirements for `MembershipFactory.sol::createNewDAOMembership()` and `MembershipFactory.sol::joinDAO()` despite platform requirements; Any address can create or join a DAO.

Description:

The MembershipFactory contract lacks validation that:

  1. DAO creators have completed required KYC/AML process

  2. DAO joiners own a pseudo-KYC Identity NFT

From One World Project's website:

DAO Creation: Sign Up and Verify - Create an account and complete the KYC/AML process.
DAO Membership: Purchase an Identity NFT, which serves as your pseudo-KYC and digital business card. This NFT verifies your identity and grants you access to the platform's features.

However, examining MembershipFactory.sol shows no validation at the contract level for either requirement:

// No KYC check for DAO creation:
function createNewDAOMembership(DAOInputConfig calldata daoConfig, TierConfig[] calldata tierConfigs) external returns (address) { require(currencyManager.isCurrencyWhitelisted(daoConfig.currency), "Currency not accepted."); require(daoConfig.noOfTiers == tierConfigs.length, "Invalid tier input.");
require(daoConfig.noOfTiers > 0 && daoConfig.noOfTiers <= TIER_MAX, "Invalid tier count.");
require(getENSAddress[daoConfig.ensname] == address(0), "DAO already exist.");
// No check that msg.sender has completed KYC/AML ...
// No Identity NFT check for joining DAOs:
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.");
// No check that msg.sender owns an Identity NFT ...

Impact:

  • Complete bypass of platform's identity verification and compliance requirements

  • Any address can create DAOs without KYC/AML verification

  • Any address can join DAOs without Identity NFT verification

  • Core platform functionality that relies on verified identities is compromised

  • Reliance solely on frontend restrictions which can be circumvented by anyone who knows the contract address post-deployment.

Explanation of severity:

This is considered a high severity bug as likelihood is High(very easy to circumvent) and the impact is also High(having members be KYC'd is core principle of protocol).

Security through obscurity is not a valid approach.

If it was the intention of the protocol to control entire business flow on the frontend, the functions should have access controls, thus the issue remains.

Proof of Code:

  1. Create a new test folder and ensure foundry.toml::profile.default::test points to test folder.

  2. Create a contract in the folder with the following code:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {MembershipFactory} from "../contracts/dao/MembershipFactory.sol"; // Import PATH may alter
import {CurrencyManager} from "../contracts/dao/CurrencyManager.sol"; // Import PATH may alter
import {MembershipERC1155} from "../contracts/dao/tokens/MembershipERC1155.sol"; // Import PATH may alter
import {DAOType, DAOInputConfig, TierConfig} from "../contracts/dao/libraries/MembershipDAOStructs.sol";
import {OWPERC20} from "../contracts/shared/testERC20.sol"; // Import PATH may alter
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MembershipFactoryTest is Test {
MembershipFactory public factory;
CurrencyManager public currencyManager;
MembershipERC1155 public implementation;
OWPERC20 public testToken;
address public owpWallet;
function setUp() public {
owpWallet = makeAddr("owpWallet");
currencyManager = new CurrencyManager();
implementation = new MembershipERC1155();
testToken = new OWPERC20("testToken", "TEST");
factory = new MembershipFactory(
address(currencyManager),
owpWallet,
"baseUri/",
address(implementation)
);
bytes32 ADMIN_ROLE = currencyManager.ADMIN_ROLE();
vm.prank(address(this));
currencyManager.grantRole(ADMIN_ROLE, address(this));
currencyManager.addCurrency(address(testToken));
}
function test_CanCreateDAOWithoutKYC() public {
address nonKycCreator = makeAddr("nonKycCreator");
DAOInputConfig memory config = DAOInputConfig({
ensname: "test-dao",
daoType: DAOType.PUBLIC,
currency: address(testToken),
maxMembers: 100,
noOfTiers: 3
});
TierConfig[] memory tiers = new TierConfig[]();
tiers[0] = TierConfig({amount: 10, price: 100, power: 1, minted: 0});
tiers[1] = TierConfig({amount: 10, price: 200, power: 2, minted: 0});
tiers[2] = TierConfig({amount: 10, price: 300, power: 3, minted: 0});
// Non-KYC'd address can create DAO
vm.prank(nonKycCreator);
address daoAddress = factory.createNewDAOMembership(config, tiers);
assertTrue(daoAddress != address(0), "DAO creation should succeed without KYC");
}
function test_CanJoinDAOWithoutIdentityNFT() public {
// First create a DAO
DAOInputConfig memory config = DAOInputConfig({
ensname: "test-dao",
daoType: DAOType.PUBLIC,
currency: address(testToken),
maxMembers: 100,
noOfTiers: 3
});
TierConfig[] memory tiers = new TierConfig[]();
tiers[0] = TierConfig({amount: 10, price: 100, power: 1, minted: 0});
tiers[1] = TierConfig({amount: 10, price: 200, power: 2, minted: 0});
tiers[2] = TierConfig({amount: 10, price: 300, power: 3, minted: 0});
address daoAddress = factory.createNewDAOMembership(config, tiers);
// Create joiner who has no Identity NFT
address joiner = makeAddr("joiner");
// Deal Joinier testToken to join dao and give approval
uint256 amountForJoiner = 100;
deal(address(testToken), joiner, amountForJoiner);
vm.prank(joiner);
IERC20(address(testToken)).approve(address(factory), amountForJoiner);
// Someone without Identity NFT can join
vm.prank(joiner);
vm.expectEmit();
emit MembershipFactory.UserJoinedDAO(joiner, daoAddress, 0);
factory.joinDAO(daoAddress, 0); // Should not revert
// Show membership ERC1155 has been minted to joiner
TierConfig[] memory updatedTiers = factory.tiers(daoAddress);
assertEq(updatedTiers[0].minted, 1, "User joined without Identity NFT");
}
}
  1. run forge test.

Tools Used:

Manual review and custom Forge test suite - Convert Hardhat project to Foundry

Recommended Mitigation:

  1. Add access controls if the intention was frontend control from the protocol.

  2. Implement on-chain KYC tracking for DAO creation.

  3. Add Identity NFT check to DAO joining:

function joinDAO(address daoMembershipAddress, uint256 tierIndex) external {
+ require(identityNFT.balanceOf(msg.sender) > 0, "Must own Identity NFT to join DAO");
require(daos[daoMembershipAddress].noOfTiers > tierIndex, "Invalid tier."); ... }
Updates

Lead Judging Commences

0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

casinocompiler Submitter
about 1 year ago
0xbrivan2 Lead Judge
about 1 year ago
0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

KYC is not checked when creating or joining DAOs

Support

FAQs

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

Give us feedback!