Vulnerability Details
Wrong check for divorce status in Airdrop.claim()
lead to possibility to collect LoveToken even if the user is divorced.
POC
When we call soulmateContract.isDivorced()
from Airdrop.claim()
the msg.sender
will be Airdrop contract address, who can never be in a Soulmate.divorced
mapping.
Add this test to AirdropTest.t.sol
and run via forge test --mt test_Divorce
to see it success.
function test_Divorce() public {
_mintOneTokenForBothSoulmates();
vm.prank(soulmate1);
soulmateContract.getDivorced();
assertTrue(soulmateContract.isDivorced() == true);
vm.warp(block.timestamp + 200 days + 1 seconds);
vm.prank(soulmate1);
airdropContract.claim();
assertTrue(loveToken.balanceOf(soulmate1) == 200 ether);
vm.prank(soulmate2);
airdropContract.claim();
assertTrue(loveToken.balanceOf(soulmate2) == 200 ether);
}
Output:
Running 1 test for test/unit/AirdropTest.t.sol:AirdropTest
[PASS] test_Divorce() (gas: 403677)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.84ms
Tools Used
Manual review, foundry.
Recommendations
Make the following changes in Solmate.isDivorced()
https://github.com/Cyfrin/2024-02-soulmate/blob/b3f9227942ffd5c443ce6bccaa980fea0304c38f/src/Soulmate.sol#L131C1-L133C6
- function isDivorced() public view returns (bool) {
+ function isDivorced(address soulmate) public view returns (bool) {
- return divorced[msg.sender];
+ return divorced[soulmate];
}
Make the following changes in ISoulmate
https://github.com/Cyfrin/2024-02-soulmate/blob/b3f9227942ffd5c443ce6bccaa980fea0304c38f/src/interface/ISoulmate.sol#L28
interface ISoulmate {
...
- function isDivorced() external view returns (bool);
+ function isDivorced(address soulmate) external view returns (bool);
...
}
Make the following changes in Airdrop.claim()
https://github.com/Cyfrin/2024-02-soulmate/blob/b3f9227942ffd5c443ce6bccaa980fea0304c38f/src/Airdrop.sol#L53
function claim() public {
// No LoveToken for people who don't love their soulmates anymore.
- if (soulmateContract.isDivorced()) revert Airdrop__CoupleIsDivorced();
+ if (soulmateContract.isDivorced(msg.sender)) revert Airdrop__CoupleIsDivorced();
// Calculating since how long soulmates are reunited
uint256 numberOfDaysInCouple = (block.timestamp -
soulmateContract.idToCreationTimestamp(
soulmateContract.ownerToId(msg.sender)
)) / daysInSecond;
uint256 amountAlreadyClaimed = _claimedBy[msg.sender];
if (
amountAlreadyClaimed >=
numberOfDaysInCouple * 10 ** loveToken.decimals()
) revert Airdrop__PreviousTokenAlreadyClaimed();
uint256 tokenAmountToDistribute = (numberOfDaysInCouple *
10 ** loveToken.decimals()) - amountAlreadyClaimed;
// Dust collector
if (
tokenAmountToDistribute >=
loveToken.balanceOf(address(airdropVault))
) {
tokenAmountToDistribute = loveToken.balanceOf(
address(airdropVault)
);
}
_claimedBy[msg.sender] += tokenAmountToDistribute;
emit TokenClaimed(msg.sender, tokenAmountToDistribute);
loveToken.transferFrom(
address(airdropVault),
msg.sender,
tokenAmountToDistribute
);
}