TempleGold

TempleDAO
Foundry
25,000 USDC
View results
Submission Details
Severity: medium
Invalid

Inaccurate Token Calculations Due to Fixed Decimal Values in TempleGoldStaking.sol

File Location: protocol/contracts/templegold/TempleGoldStaking.sol#L463-L482#L484-L493#L502-513#L589-L602

Vulnerability Details

The use of fixed decimal values such as 1e18 or 1e8 in Solidity contracts can lead to inaccuracies, bugs, and vulnerabilities, particularly when interacting with tokens having different decimal configurations. Not all ERC20 tokens follow the standard 18 decimal places, and assumptions about decimal places can lead to miscalculations. Always retrieve and use the decimals() function from the token contract itself when performing calculations involving token amounts.

Impact

  • Miscalculation of Rewards

  • Financial Vulnerability

  • Financial Vulnerability

  • Reputation

  • Platform Instability

  • Potential Exploitation

Tools Used

  • Inspection manual

  • Solidity

Recommendations

Use the 'decimals()' Function of the Token Contract:

  • Always retrieve the decimal value of the token using the 'decimals()' function of the ERC20 token contract in use. This ensures that all calculations involving the number of tokens conform to the decimal configuration of those tokens.

Update All Calculations Using Dynamic Decimal Values:

  • Replace all constant decimal values ​​(1e18 or 1e8) with the values ​​obtained from the 'decimals()' token. Make sure reward calculations, vesting rates, and other calculations use dynamic decimal values.

Code snippet:

L463-L482 look L476 and L480

function _earned(
StakeInfo memory _stakeInfo,
address _account,
uint256 _index
) internal view returns (uint256) {
uint256 vestingRate = _getVestingRate(_stakeInfo);
if (vestingRate == 0) {
return 0;
}
uint256 _perTokenReward;
if (vestingRate == 1e18) {
_perTokenReward = _rewardPerToken();
} else {
_perTokenReward = _rewardPerToken() * vestingRate / 1e18;
}
return
(_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / 1e18 +
claimableRewards[_account][_index];
}

L484-L493 look L491

function _getVestingRate(StakeInfo memory _stakeInfo) internal view returns (uint256 vestingRate) {
if (_stakeInfo.stakeTime == 0) {
return 0;
}
if (block.timestamp > _stakeInfo.fullyVestedAt) {
vestingRate = 1e18;
} else {

L502-L513 look L509-L511

function _rewardPerToken() internal view returns (uint256) {
if (totalSupply == 0) {
return rewardData.rewardPerTokenStored;
}
return
rewardData.rewardPerTokenStored +
(((_lastTimeRewardApplicable(rewardData.periodFinish) -
rewardData.lastUpdateTime) *
rewardData.rewardRate * 1e18)
/ totalSupply);
}

L589-L602 look L598

modifier updateReward(address _account, uint256 _index) {
{
// stack too deep
rewardData.rewardPerTokenStored = uint216(_rewardPerToken());
rewardData.lastUpdateTime = uint40(_lastTimeRewardApplicable(rewardData.periodFinish));
if (_account != address(0)) {
StakeInfo memory _stakeInfo = _stakeInfos[_account][_index];
uint256 vestingRate = _getVestingRate(_stakeInfo);
claimableRewards[_account][_index] = _earned(_stakeInfo, _account, _index);
userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / 1e18;
}
}
_;
}

Fixed code:

interface IERC20 {
function decimals() external view returns (uint8);
}
contract TempleGoldStaking {
struct StakeInfo {
uint256 amount;
uint256 stakeTime;
uint256 fullyVestedAt;
}
struct RewardData {
uint256 rewardPerTokenStored;
uint256 lastUpdateTime;
uint256 periodFinish;
uint256 rewardRate;
}
mapping(address => mapping(uint256 => uint256)) public userRewardPerTokenPaid;
mapping(address => mapping(uint256 => uint256)) public claimableRewards;
mapping(address => mapping(uint256 => StakeInfo)) private _stakeInfos;
uint256 public totalSupply;
uint256 public vestingPeriod;
RewardData public rewardData;
IERC20 public token;
constructor(address _token) {
token = IERC20(_token);
}
function _earned(
StakeInfo memory _stakeInfo,
address _account,
uint256 _index
) internal view returns (uint256) {
uint8 tokenDecimals = token.decimals();
uint256 vestingRate = _getVestingRate(_stakeInfo);
if (vestingRate == 0) {
return 0;
}
uint256 _perTokenReward;
if (vestingRate == 10 ** tokenDecimals) {
_perTokenReward = _rewardPerToken();
} else {
_perTokenReward = _rewardPerToken() * vestingRate / (10 ** tokenDecimals);
}
return
(_stakeInfo.amount * (_perTokenReward - userRewardPerTokenPaid[_account][_index])) / (10 ** tokenDecimals) +
claimableRewards[_account][_index];
}
function _getVestingRate(StakeInfo memory _stakeInfo) internal view returns (uint256 vestingRate) {
uint8 tokenDecimals = token.decimals();
if (_stakeInfo.stakeTime == 0) {
return 0;
}
if (block.timestamp > _stakeInfo.fullyVestedAt) {
vestingRate = 10 ** tokenDecimals;
} else {
vestingRate = (block.timestamp - _stakeInfo.stakeTime) * (10 ** tokenDecimals) / vestingPeriod;
}
}
function _rewardPerToken() internal view returns (uint256) {
uint8 tokenDecimals = token.decimals();
if (totalSupply == 0) {
return rewardData.rewardPerTokenStored;
}
return
rewardData.rewardPerTokenStored +
(((_lastTimeRewardApplicable(rewardData.periodFinish) -
rewardData.lastUpdateTime) *
rewardData.rewardRate * (10 ** tokenDecimals))
/ totalSupply);
}
modifier updateReward(address _account, uint256 _index) {
{
uint8 tokenDecimals = token.decimals();
// stack too deep
rewardData.rewardPerTokenStored = uint216(_rewardPerToken());
rewardData.lastUpdateTime = uint40(_lastTimeRewardApplicable(rewardData.periodFinish));
if (_account != address(0)) {
StakeInfo memory _stakeInfo = _stakeInfos[_account][_index];
uint256 vestingRate = _getVestingRate(_stakeInfo);
claimableRewards[_account][_index] = _earned(_stakeInfo, _account, _index);
userRewardPerTokenPaid[_account][_index] = vestingRate * uint256(rewardData.rewardPerTokenStored) / (10 ** tokenDecimals);
}
}
_;
}
function _lastTimeRewardApplicable(uint256 periodFinish) internal view returns (uint256) {
return block.timestamp < periodFinish ? block.timestamp : periodFinish;
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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