Project

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

An attacker can consistently steal funds from and DoS the EXTERNAL_CALLER role through the `NativeMetaTransaction::executeMetaTransaction`

Summary

The NativeMetaTransaction::executeMetaTransaction allows a user to specify any address as the userAddress instead of using msg.sender.

https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/meta-transaction/NativeMetaTransaction.sol#L33-L39

Also, ecrecover is used to verify metatransaction signer's signature instead of openzepplin ECDSA library.

https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/meta-transaction/NativeMetaTransaction.sol#L100

ecrecover is vulnerable to signature malleability attack in which users' signatures can be replayed to sign data and the signatures will be valid.

Combining these two vulnerabilities, a malicious user can call some functions in the contract using any desired address. An attacker can exploit these vulnerabilities to steal funds from the EXTERNAL_CALLER role and DoS him/her in the process each time the EXTERNAL_CALLER role tries to use MembershipFactory::callExternalContract function to transfer a whitelisted ERC20 token. The details of the attack path is described in the attack path section below.

Vulnerability Details

The NativeMetaTransaction::executeMetaTransaction allows a user to specify any address as the userAddress instead of using msg.sender.

https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/meta-transaction/NativeMetaTransaction.sol#L33-L39

s parameter in a signature has two valid points on the elliptic curve - one below n/2 and the other above n/2. An attacker using one of the valid (v, r and s combination) signatures can calculate the other valid signature on the other side of n/2.

This allows a malicious user to call some functions in the contract using any desired address.

Impact

  1. Funds get stolen from the EXTERNAL_CALLER role wallet.

  2. The EXTERNAL_CALLER ERC20 transfer transaction with MembershipFactory::callExternalContract will also fail causing denial of service.

Attack Path

  1. An attacker use the previous signature of an EXTERNAL_CALLER role wallet to calculate another valid signature;

  2. the attacker observes the EXTERNAL_CALLER role wallet approval transaction for the MembershipFactory::callExternalContract function to spend a whitelisted currency on its behalf in the mempool;

  3. the attacker can quickly create a dao with the specified whitelisted currency and DAOConfig::tiers::price = EXTERNAL_CALLER wallet approved token value.

  4. After the approval is executed, the attacker can now front-run the main EXTERNAL_CALLER transaction by sending a transaction to MembershipFactory::joinDAO through NativeMetaTransaction::executeMetaTransaction with EXTERNAL_CALLER wallet address as the userAddress and its replayed signature;

  5. MembershipFactory::_msgSender will return EXTERNAL_CALLER address as the caller address (or _msgSender) and 80% of the approved tokens will be successfully transferred from the EXTERNAL_CALLER wallet to the attacker's specified daoMembershipAddress;

  6. Since the MembershipERC1155 contract does not have ERC20 withdrawal function the stolen tokens will remain stuck in the contract.

  7. The EXTERNAL_CALLER main transaction to use MembershipFactory::callExternalContract for a transfer will fail as the approval allowance has already being spent by the attacker.

Tools Used

Manual review

Recommendations

Refactor the NativeMetaTransaction::executeMetaTransaction as shown below and use Openzepplin ECDSA library instead of ecrecover to verify signatures.

function executeMetaTransaction(
- address userAddress,
bytes memory functionSignature,
bytes32 sigR,
bytes32 sigS,
uint8 sigV
) public payable returns (bytes memory) {
MetaTransaction memory metaTx = MetaTransaction({
nonce: nonces[userAddress],
- from: userAddress,
+ from: msg.sender,
functionSignature: functionSignature
});
require(
- verify(userAddress, metaTx, sigR, sigS, sigV),
+ verify(msg.sender, metaTx, sigR, sigS, sigV),
"Signer and signature do not match"
);
// increase nonce for user (to avoid re-use)
nonces[userAddress] = nonces[userAddress] + 1;
emit MetaTransactionExecuted(
- userAddress,
msg.sender,
functionSignature,
hashMetaTransaction(metaTx)
);
// Append userAddress and relayer address at the end to extract it from calling context
(bool success, bytes memory returnData) = address(this).call{value: msg.value}(
- abi.encodePacked(functionSignature, userAddress)
+ abi.encodePacked(functionSignature, msg.sender)
);
require(success, "Function call not successful");
return returnData;
}
Updates

Lead Judging Commences

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

Support

FAQs

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

Give us feedback!