Platform: Code4rena
Start Date: 29/03/2022
Pot Size: $50,000 USDC
Total HM: 16
Participants: 42
Period: 5 days
Judge: 0xean
Total Solo HM: 9
Id: 105
League: ETH
Rank: 12/42
Findings: 2
Award: $1,120.98
π Selected for report: 2
π Solo Findings: 0
560.4852 USDC - $560.49
https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1131
Consider the following scenario: Day 0: User A stakes 200 tokens and calls the cooldown function. At this time, user A's cooldown is Day 0. Day 15: User B stakes 100 tokens, but then wants to unstake tokens. So user A said that he could assist user B in unstaking tokens, and this could be done by deploying a smart contract. In the smart contract deployed by user A, user B first needs to transfer 100 tokens to user A. In the _getNewReceiverCooldown function, _senderCooldown is Day 15 and receiverCooldown is Day 0, so the latest cooldown of user A is (100 * Day 15 + 200 * Day 0)/(100+200) = Day 5.
function _getNewReceiverCooldown( uint256 senderCooldown, uint256 amount, address receiver, uint256 receiverBalance ) internal view returns(uint256) { uint256 receiverCooldown = cooldowns[receiver]; // If receiver has no cooldown, no need to set a new one if(receiverCooldown == 0) return 0; uint256 minValidCooldown = block.timestamp - (COOLDOWN_PERIOD + UNSTAKE_PERIOD); // If last receiver cooldown is expired, set it back to 0 if(receiverCooldown < minValidCooldown) return 0; // In case the given senderCooldown is 0 (sender has no cooldown, or minting) uint256 _senderCooldown = senderCooldown < minValidCooldown ? block.timestamp : senderCooldown; // If the sender cooldown is better, we keep the receiver cooldown if(_senderCooldown < receiverCooldown) return receiverCooldown; // Default new cooldown, weighted average based on the amount and the previous balance return ((amount * _senderCooldown) + (receiverBalance * receiverCooldown)) / (amount + receiverBalance); }
Since User A is still at UNSTAKE_PERIOD after receiving the tokens, User A unstakes 100 tokens and sends it to User B.
After calculation, we found that when user A has a balance of X and is at the edge of UNSTAKE_PERIOD, user A can assist in unstaking the X/2 amount of tokens just staked.
https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L1131
None
After calculation, we found that the number of tokens that users at the edge of UNSTAKE_PERIOD can assist in unstaking conforms to the following equation UNSTAKE_PERIOD/COOLDOWN_PERIOD = UNSTAKE_AMOUNT/USER_BALANCE, when COOLDOWN_PERIOD remains unchanged, the smaller the UNSTAKE_PERIOD, the less tokens the user can assist in unstaking, so UNSTAKE_PERIOD can be adjusted to alleviate this situation.
#0 - Kogaroshi
2022-03-31T18:22:04Z
Reduced the Unstake Period to 2 days to reduce the possibility of such scenario https://github.com/PaladinFinance/Paladin-Tokenomics/pull/4
560.4852 USDC - $560.49
https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L891-L905
In the _beforeTokenTransfer function, cooldowns will be set to 0 when the user transfers all tokens to himself. Consider the following scenario Day 0: The user stakes 100 tokens and calls the cooldown function Day 10: the user wanted to unstake the tokens, but accidentally transferred all the tokens to himself, which caused the cooldown to be set to 0 and the user could not unstake.
https://github.com/code-423n4/2022-03-paladin/blob/main/contracts/HolyPaladinToken.sol#L891-L905
None
function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual override { if(from != address(0)) { //check must be skipped on minting // Only allow the balance that is unlocked to be transfered require(amount <= _availableBalanceOf(from), "hPAL: Available balance too low"); } // Update user rewards before any change on their balance (staked and locked) _updateUserRewards(from); uint256 fromCooldown = cooldowns[from]; //If from is address 0x00...0, cooldown is always 0 if(from != to) { // Update user rewards before any change on their balance (staked and locked) _updateUserRewards(to); // => we don't want a self-transfer to double count new claimable rewards // + no need to update the cooldown on a self-transfer uint256 previousToBalance = balanceOf(to); cooldowns[to] = _getNewReceiverCooldown(fromCooldown, amount, to, previousToBalance); // If from transfer all of its balance, reset the cooldown to 0 uint256 previousFromBalance = balanceOf(from); if(previousFromBalance == amount && fromCooldown != 0) { cooldowns[from] = 0; } } }
#0 - Kogaroshi
2022-03-31T11:16:49Z
Issue fixed in the PR: https://github.com/PaladinFinance/Paladin-Tokenomics/pull/2 Applying the recommended mitigation. An extra test for that scenario was added to the hPAL tests.
#1 - 0xean
2022-04-08T20:27:37Z
Based on additional information this doesn't permanently lock user funds, but it does unintentionally extend the duration of the lock.
See #88
This effectively means that user funds be frozen for an additional period, which is the difference between his former cooldown and current timestamp. There is no reason for that and it will go against userβs expectations
Therefore, i agree this should be a medium severity issue and will mark all duplicates as such