The Staking::claimRewards()
function incorrectly tracks timestamp for msg.sender
and allows anyone to claim token rewards without having to wait 1 week since a deposit.
The Staking::claimRewards()
function is meant to give tokens to those who have deposited tokens into the contract for a specific time. For example, if 1 token is deposited, they can claim 1 token after 1 week, however the contract does not follow the intended behavior. The issue lies when setting the lastClaim
local variable:
When the contract is first called, the function declares and sets the soulmateId
of msg.sender
by calling the Soulmate::ownerToId()
function. This will set the soulmate NFT ID that belongs to msg.sender
. It's worth noting, if msg.sender
has never claimed a soulmate via Soulmate::mintSoulmateToken()
this will set the soulmateId
to 0, while if the msg.sender
is awaiting a soulmate, this will set soulmateId
to the NFT ID that will be minted.
The function proceeds to see if lastClaim[msg.sender]
equals 0. If we assume this is the first time a soulmate attempts to claim, it will call Soulmate::idToCreationTimestamp()
function and set lastClaim[msg.sender]
to the timestamp their soulmate NFT was created. For a user who has no soulmate, this will set lastClaim[msg.sender]
to the time the first soulmate NFT was created. For a user who is still awaiting a soulmate, it will set lastClaim[msg.sender]
to 0 as the timestamp of the next soulmate NFT ID doesn't exist yet.
If we then look at how then the timeInWeeksSinceLastClaim
local variable is set:
This takes the current block.timestamp
and minuses it from lastClaim[msg.sender]
. This results in the time since the soulmate NFT was created, and not when tokens were deposited into the staking contract.
For a user who has never minted a soulmate, This results in the time since the first soulmate NFT was created, and not when tokens were deposited into the staking contract.
In a case where a user is awaiting a soulmate, block.timestamp - 0
will result in just block.timestamp
. Thus the calculation for timeInWeeksSinceLastClaim
will just be block.timestamp / 1 weeks
(604800). If we assume block.timestamp
is equal to X
amount of weeks, this user will be rewarded X
amount of tokens regardless of when their tokens were deposited.
It's worth noting if no soulmate NFTs have been minted, there should be no way for tokens to be deposited assuming the only way to claim tokens is initially through the Airdrop::claim()
function.
A soulmate is rewarded tokens based on when their soulmate NFT was created regardless of the time they deposited.
A user who has never minted a soulmate will be rewarded tokens based on when the first soulmate NFT was created regardless of the time they deposited.
A user awaiting a soulmate but has deposited tokens can be rewarded tokens based on current block.timestamp
divided by 604800 (1 weeks
) regardless of the time they deposited.
We can imagine the most-likely scenario and how it plays out:
User A calls Soulmate::mintSoulmateToken()
.
User B calls Soulmate::mintSoulmateToken()
and now NFT #0 has been minted.
7 days passes by since NFT #0 was minted.
User A calls Airdrop::claim()
and now has 7 lovetokens.
User A transfers 7 lovetokens to user C.
User C deposits 7 lovetokens to Staking contract.
User C calls Soulmate::mintSoulmateToken()
and is awaiting a soulmate.
User C calls Staking::claimRewards()
.
Below you can see a POC of the above scenario including how block.timestamp
affects rewards:
VS Code, Foundry
Consider creating a lastDeposit
mapping and setting the mapping in Staking::deposit()
for a deposit and reference that timestamp in Staking::claimRewards()
for the very first claim of an user. You'll also want to include a check to make sure a user has deposited. Note that this could "reset" the ability to claim if the user makes multiple deposits prior to their first claim:
High severity, this allows users to claim additional rewards without committing to intended weekly staking period via multi-deposit/deposit right before claiming rewards.
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
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.