Summary
The lack of support for fee-on-transfer tokens in the joinDAO
function presents a critical vulnerability that could lead to accounting discrepancies and fund losses for DAOs using such tokens as their membership currency. When fee-on-transfer tokens are used, the actual amount received by the platform and DAO will be less than the calculated amounts due to the token's internal fee mechanism. This creates a mismatch between expected and actual received amounts, potentially leading to significant financial losses over time. The severity is particularly high as popular tokens like SafeMoon or similar tokens with transfer taxes could be whitelisted through the currency manager, making this vulnerability immediately exploitable.
The issue lies in the function's assumption that the amount specified in transferFrom is equal to the amount received by the recipient. The contract calculates platform fees and DAO amounts based on the pre-fee tierPrice, but fee-on-transfer tokens deduct their fees from each transfer, resulting in recipients receiving less than the calculated amounts. This architectural oversight means that both the platform (owpWallet) and the DAO receive less than their intended share, breaking the economic model of the membership system. Furthermore, since the minting of NFT membership is tied to these transfers, users effectively receive full membership benefits while paying reduced actual amounts.
https://github.com/Cyfrin/2024-11-one-world/blob/main/contracts/dao/MembershipFactory.sol#L140
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);
}
Recommended Fix
Implement balance checks before and after transfers to ensure correct amounts are received, and adjust the transfer amounts to account for fees:
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;
require(tierPrice > 0, "Invalid tier price");
uint256 platformFees = (tierPrice * platformFeeBps) / 10000;
uint256 daoAmount = tierPrice - platformFees;
IERC20 token = IERC20(daos[daoMembershipAddress].currency);
uint256 initialPlatformBalance = token.balanceOf(owpWallet);
uint256 initialDAOBalance = token.balanceOf(daoMembershipAddress);
token.transferFrom(_msgSender(), owpWallet, platformFees);
token.transferFrom(_msgSender(), daoMembershipAddress, daoAmount);
uint256 platformReceived = token.balanceOf(owpWallet) - initialPlatformBalance;
uint256 daoReceived = token.balanceOf(daoMembershipAddress) - initialDAOBalance;
require(platformReceived >= (platformFees * 95) / 100, "Platform fee transfer failed");
require(daoReceived >= (daoAmount * 95) / 100, "DAO payment transfer failed");
daos[daoMembershipAddress].tiers[tierIndex].minted += 1;
IMembershipERC1155(daoMembershipAddress).mint(_msgSender(), tierIndex, 1);
emit UserJoinedDAO(_msgSender(), daoMembershipAddress, tierIndex);
emit TokensReceived(platformReceived, daoReceived);
}
event TokensReceived(uint256 platformReceived, uint256 daoReceived);