2022-10-merit-circle-judging's People
2022-10-merit-circle-judging's Issues
Chom - cumulativeRewardsOf can be minus. This is unexpected behavior
Chom
medium
cumulativeRewardsOf can be minus. This is unexpected behavior
Summary
cumulativeRewardsOf can be minus. This is unexpected behavior.
Vulnerability Detail
Transferring from A to a new wallet B deducts pointsCorrection to B. Since B is a new wallet -> pointsCorrection = 0, when deducted pointsCorrection will be negative.
function _correctPointsForTransfer(address _from, address _to, uint256 _shares) internal {
int256 _magCorrection = (pointsPerShare * _shares).toInt256();
pointsCorrection[_from] = pointsCorrection[_from] + _magCorrection;
pointsCorrection[_to] = pointsCorrection[_to] - _magCorrection;
}
function cumulativeRewardsOf(address _account) public view override returns (uint256) {
return ((pointsPerShare * getSharesOf(_account)).toInt256() + pointsCorrection[_account]).toUint256() / POINTS_MULTIPLIER;
}
In case pointsPerShare == 0, (pointsPerShare * getSharesOf(_account)) = 0 but pointsCorrection[_account] is minus. As a result, cumulativeRewardsOf is minus. Finally, everything that uses cumulativeRewardsOf will revert.
Impact
cumulativeRewardsOf can be minus. This is unexpected behavior as it should return zero or positive uint256. In this case, anyone can force a random wallet to be in debt.
Code Snippet
Tool used
Manual Review
Recommendation
Check the logic of point correction / cumulativeRewardsOf again
Duplicate of #26
csanuragjain - User might lose funds on extending lock
csanuragjain
high
User might lose funds on extending lock
Summary
While extending lock, if the resulting mintAmount becomes lesser than userDeposit.shareAmount (due to varying Multiplier curve) then user own deposit are burned which means user loses funds for extending duration which is wrong
Vulnerability Detail
- User had a deposit of amount 100 for duration X which gave him mint Amount of 50
- After some time, User wants to extend duration by y ie new duration should be X+y using extendLock
- Due to varying Multiplier curve, the extendLock function resulted in new mint Amount of 40 with the duration X+y
- Since the new mint Amount of 40 is lesser than userDeposit.shareAmount so user deposit are burned which does not make sense since user is penalized for keeping his funds locked for more duration (instead he should be rewarded for locking amount)
function extendLock(uint256 _depositId, uint256 _increaseDuration) external {
else if (mintAmount < userDeposit.shareAmount) {
depositsOf[_msgSender()][_depositId].shareAmount = mintAmount;
_burn(_msgSender(), userDeposit.shareAmount - mintAmount);
}
}
Impact
User will lose there funds
Code Snippet
Tool used
Manual Review
Recommendation
Revert if mintAmount < userDeposit.shareAmount so that user is prevented from loss. In this case it is advisable for user to start a new deposit
Lambda - setCurvePoint: unit not updated when curve.length changes
Lambda
medium
setCurvePoint: unit not updated when curve.length changes
Summary
When curve.length
changes in setCurvePoint
, unit
is not updated, leading to wrong calculations.
Vulnerability Detail
unit
is per definition maxLockDuration / (curve.length - 1)
and when curve.length
changes, it should therefore be recalculated (which is done in setCurve
). However, the length of the curve
array can also change in setCurvePoint
(increase or decrease) and unit
is not recalculated in these cases.
Impact
getMultiplier
will return wrong values (a multiplier that is too high or low, which causes a financial loss / unexpected financial gain for users) and even not work at all in certain scenarios. For instance, it can happen that uint n = _lockDuration / unit > curve.length - 1
, which causes the function to revert (because an out-of-bounds array access).
Code Snippet
Tool used
Manual Review
Recommendation
Recalculate unit
when the length of curve
changes.
Duplicate of #101
rvierdiiev - User can't transfer all amount of claimed rewards
rvierdiiev
medium
User can't transfer all amount of claimed rewards
Summary
User can't transfer all amount of claimed rewards.
Vulnerability Detail
There is developer error in BasePool.claimRewards
function. It's not possible to transfer value that less then 2. See here. That means that when amount nonEscrowedRewardAmount == 1
it will not be transferred to receiver and receiver will lost it.
Impact
Receiver lose funds.
Code Snippet
function claimRewards(address _receiver) external {
uint256 rewardAmount = _prepareCollect(_msgSender());
uint256 escrowedRewardAmount = rewardAmount * escrowPortion / 1e18;
uint256 nonEscrowedRewardAmount = rewardAmount - escrowedRewardAmount;
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
}
// ignore dust
if(nonEscrowedRewardAmount > 1) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
emit RewardsClaimed(_msgSender(), _receiver, escrowedRewardAmount, nonEscrowedRewardAmount);
}
Tool used
Manual Review
Recommendation
Change check condition to if(nonEscrowedRewardAmount > 0)
.
carlitox477 - TimeLockPool#increaseLock allows burning shares
carlitox477
medium
TimeLockPool#increaseLock allows burning shares
Summary
This can be done by setting _receiver=address(0)
leading to lost of users deposit token
Vulnerability Detail
Calling increaseLock with _receiver=address(0)
will allow msg.sender
to send their deposit token to address zero.
Impact
Allows users to burn their rewards
Code Snippet
Tool used
Manual Review
Recommendation
Add next require statement at the start of function increaseLock: require(_receiver != address(0))
CodingNameKiki - A mistake made by depositer will result into losing his funds, which will be stuck in the pool.
CodingNameKiki
high
A mistake made by depositer will result into losing his funds, which will be stuck in the pool.
Summary
A mistake made by the depositer will result into losing his funds, which will be stuck in the pool.
Vulnerability Detail
Example:
Kiki wants to lock his funds and calls the function deposit()
. He locks his 5000 tokens for the duration of 2 weeks and provides his address as the "_receiver".
By calling the function deposit()
:
1.The tokens will be transferred from Kiki to the pool.
2.The mintAmount
will be calculated
3.Kiki's deposit information will be stored into a new Deposit struct in the depositOf mapping.
4.After that the function will mint the corresponding shares for the given amount of tokens to Kiki.
Later Kiki decides that he wants to lock another 1000 of his tokens. Instead of calling the function increaseLock()
, he mades the mistake and calls the function deposit()
again. This time he provides the amount of 1000 tokens and the same duration of 2 weeks.
The function will push the new deposit information to the already existing Deposit struct from the first deposit Kiki made.
However the problem here is that the function will change the amount of 5000 tokens from the first deposit
with the new deposit of 1000 tokens and won't actually sum both of the amounts.
Same happens for the shares as well, the old "shareAmount" corresponding the amount of 5000 tokens
will be replaced with the new shares from the second deposited amount of 1000 tokens.
2 weeks later when Kiki calls the function withdraw()
to withdraw his tokens. The function will delete Kiki's Deposit struct, will burn the shares and return the tokens. However duo to the fact that userDeposit.shareAmount
and userDeposit.amount
will be equal to the 1000 tokens from the second deposit Kiki made. The function will successfuly return only the 1000 tokens to Kiki.
But his other 5000 tokens from the first deposit he made, will be stuck in the pool.
Kiki won't be able to withdraw his first deposit of 5000 tokens.
Because the function withdraw()
checks the deposit information from the Kiki's Deposit struct
and both userDeposit.amount
and userDeposit.shareAmount
will be equal to the second deposit Kiki made for 1000 tokens.
Duo to the fact that the deposit()
function minted shares for both of the deposits Kiki made, and only the shares of the second deposit were burned corresponding the 1000 tokens. Kiki won't only lose his first deposit of 5000 tokens, but won't even get the chance to claim the full amount of rewards, he will be able to claim the rewards only for the burned amount of shares corresponding the 1000 tokens.
Simple mistake led to Kiki's losing 5000 tokens out of the 6000 tokens he deposited into the pool from the two deposits.
And by following this scenario his 5000 tokens will be stuck in the pool without a way for retrieving them back.
Impact
Duo to the exploit described in "Vulnerability Detail", a simple mistake made by the depositer will lead to losing his funds, which will be stuck in the pool.
Code Snippet
Tool used
Manual Review
Recommendation
Consider adding the following changes to avoid this kind of problem from happening:
https://gist.github.com/CodingNameKiki/e28ccf6a48df4fcf4f8231a6468e52e1
ctf_sec - User will lose their ETH if they call the batch function in BoringBatch.sol with ETH sent
ctf_sec
medium
User will lose their ETH if they call the batch function in BoringBatch.sol with ETH sent
Summary
User will lose their ETH if they call the batch function in BoringBatch.sol with ETH sent
Vulnerability Detail
The boring batch has a batch function to let user execute function in batch by constructing the calldata,
function batch(bytes[] calldata calls, bool revertOnFail) external payable {
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(calls[i]);
if (!success && revertOnFail) {
revert(_getRevertMsg(result));
}
}
}
However, this function is payable and accept ETH, but there is no function in the construct that let we withdraw ETH, then ETH sent from user will be forever lost and stuck in the contract.
Impact
User can accidentally lose their ETH when calling BoringBatch.sol#Batch
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project make this function not payable. Also I do not see why a delegatecall must be performed here,
we can change to
function batch(bytes[] calldata calls) external {
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = address(this).call(calls[i]);
require(success, 'failed');
}
}
Chom - pointsPerShare is not scaled once shares have been minted (deposit) or burned (withdraw)
Chom
high
pointsPerShare is not scaled once shares have been minted (deposit) or burned (withdraw)
Summary
pointsPerShare is not scaled once shares have been burned
Vulnerability Detail
Burning reduces total supply. For example, in beginning, there is a total of 1000 shares, and 1000 rewards have been distributed (1 / share) but suddenly 900 shares have been burned. A total of 100 shares remaining each share get 1 reward, only maximum of 100 rewards can be claimed. The remaining 900 will be wasted.
On the other hand, for minting, For example, in beginning, there is a total of 100 shares, and 100 rewards have been distributed (1 / share) but suddenly 900 shares have been minted. A total of 1000 shares remaining each share gets 1 reward. You need to have 1000 rewards in the contract, but only 100 rewards is actually in the contract.
Impact
Some rewards will be wasted forever if too many burns. And it won't be a sufficient reward if new shares get minted.
Code Snippet
Tool used
Manual Review
Recommendation
Please take a look at any Masterchef implementation such as https://github.com/pancakeswap/pancake-farm/blob/master/contracts/MasterChef.sol
- Add reward debt system
- Automatically claim the reward on each deposit and withdraw.
- Automatically update reward debt on each deposit and withdraw.
Reward debt is not like withdrawnRewards tracking, it uses current balance * pointsPerShare instead.
berndartmueller - Claiming rewards with a pool that has an escrow portion but no escrow pool set will render the escrowed rewards lost
berndartmueller
medium
Claiming rewards with a pool that has an escrow portion but no escrow pool set will render the escrowed rewards lost
Summary
When a user claims rewards from a pool that has an escrow portion (a value non-zero) but no escrow pool set (set to address(0)
), the escrowed rewards will be lost.
Vulnerability Detail
A user can claim rewards with the BasePool.claimRewards
function. Parts or all of the claimable rewards can be escrowed. The escrowed rewards are sent to the escrowPool
address. The amount of escrowed rewards are calculated with escrowPortion / 1e18
. However, if the escrowPool
address is set to address(0)
but escrowPortion
is defined to a non-zero value, the escrowed rewards are lost (this state is possible as there is no appropriate validation in the initializer function).
Impact
Calculated escrowed rewards are not claimable and will be lost.
Code Snippet
function claimRewards(address _receiver) external {
uint256 rewardAmount = _prepareCollect(_msgSender());
uint256 escrowedRewardAmount = rewardAmount * escrowPortion / 1e18;
uint256 nonEscrowedRewardAmount = rewardAmount - escrowedRewardAmount;
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
}
// ignore dust
if(nonEscrowedRewardAmount > 1) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
emit RewardsClaimed(_msgSender(), _receiver, escrowedRewardAmount, nonEscrowedRewardAmount);
}
Tool used
Manual review
Recommendation
Consider either one of the following:
- prevent a state of having an escrow portion but no escrow pool set, or
- check for the escrow pool address to be non-zero, otherwise set
nonEscrowedRewardAmount
torewardAmount
Duplicate of #107
CodingNameKiki - Two malicious users can drain a big amount of rewards up to 48 weeks, for the little lock time of 10 mins.
CodingNameKiki
high
Two malicious users can drain a big amount of rewards up to 48 weeks, for the little lock time of 10 mins.
Summary
By following the scenario in "Vulnerability Detail", two malicious users can drain rewards from the pool.
Vulnerability Detail
Example:
Kiki calls the function deposit()
and locks tokens for himself, for the duration of 48 weeks and provides his address as the _receiver
. Then he calls the function deposit()
again and locks funds for Bob as well, for the duration of 10 mins and provides Bob's address as the _receiver
. After that Kiki calls the function increaseLock()
and provides his _depositId
and Bob's address as the _receiver
.
Since Kiki is the msg.sender calling the function increaseLock()
, and provides his _depositId
.
The function will successfuly make a copy in memory from Kiki's Deposit lock with the duration of 48 weeks:
The remaingDuration
will be calculated based on the Kiki's lock information, which he made for the duration of 48 weeks.
And the mintAmount
will be calculated based on the increaseAmount
provided by Kiki and the sum of the remaingDuration
.
However the problem here is that by providing Bob's address as the _receiver
. The mapping depositsOf
will take Bob's address and will lead to Bob's lock information. Duo to the fact that Kiki made the two deposits and the _depositId
will be the same for the two deposits, but only the right address will lead to the right deposit information. This way the function increaseLock()
can be tricked to add the calculated mintAmount
based on the Kiki's 48 weeks lock to Bob's lock balance.
If l understood right from the sponsor, the _depositId is created, when the user's wallet is connected to the site.
And when Kiki did the two deposits - one for him and one for Bob. The _depositId will be the same for the two deposits.
Kiki's address will lead to Kiki's lock and Bob's address will lead to Bob's lock, even tho the _depositId is the same.
The function will successfuly add the _increaseAmount
and mintAmount
calculated based on the Kiki's 48 weeks lock duration to Bob's lock of 10 mins.
When Bob's 10 mins lock duration ends, he can successfuly claim the big amount of rewards calculated based on the Kiki's 48 week duration lock. After that Bob can withdraw the increaseAmount
provided by Kiki as well and both of them can repeat this process and successfuly drain rewards from the BasePool.
Simple Example of what will happen duo to the following scenario described above:
1.Kiki locks funds for himself for the duration of 48 weeks, and locks funds for Bob for the duration of 10 mins.
2.Kiki calls the function increaseLock()
providing his _depositId
, Bob's address as the _receiver
and _increaseAmount
of 1000 tokens.
3.The calculated mintAmount
from Kiki's 48 weeks lock and the _increaseAmount
of 1000 tokens will be added to Bob's lock.
4.After Bob's lock ends, he can claim the big amount of rewards and withdraw the _increaseAmount
of 1000 tokens provided by Kiki.
Both of them can repeat this process over and over:
1.When Bob withdraw the 1000 tokens back, the function withdraw() will delete his deposit information.
2.Kiki can lock funds for Bob again, and that's how they can repeat this over and over and drain rewards from the pool.
Impact
Duo to the exploit described in "Vulnerability Detail", two malicious users can drain rewards from the BasePool.
Code Snippet
Tool used
Manual Review
Recommendation
The fix of this is simple, consider changing to this: https://gist.github.com/CodingNameKiki/cec8dd78feeb562104e2bc0a0c0b6e87
Duplicate of #102
ctf_sec - Share can be minted to address(0) in TimeLockPool.sol#Deposit
ctf_sec
medium
Share can be minted to address(0) in TimeLockPool.sol#Deposit
Summary
Share can be minted to address(0) in TimeLockPool.sol#Deposit
Vulnerability Detail
When calling the the function deposit
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
The function does not verify if the _receiver address is address(0),
When the mint is called, the share that minted to address(0) is basically equal to burn the share.
_mint(_receiver, mintAmount);
Impact
Burning the share while deposit the token basically waste storage space and erode other user's share because address(0) is not supposed to get any reward anyway.
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project check if the receiverAddress is address(0)
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if(_receiver == address(0)) {
revert InvalidReceiver();
}
if (_amount == 0) {
revert ZeroAmountError();
}
rvierdiiev - Lock time can be avoided
rvierdiiev
high
Lock time can be avoided
Summary
User is able to avoid locking his tokens due to increaseLock
function.
Vulnerability Detail
When user deposit some amount of tokens it is going to be locked for some duration.
The protocol suppose that user can't withdraw that tokens during that time.
However it's possible to deposit and withdraw tokens without lock.
This is the flow.
- Attacker from account1 makes deposit with value 1 token for a minimum time(10 minutes). Now deposit with index 0 is created for account1.
- After 10 minutes attacker from account2 makes deposit for 1 token but for the max amount of time(to get big tokens multiplier). Now deposit with index 0 is for account2.
- Attacker from account2 calls
increaseLock(0, account1.address, bigAmount)
. It's important here to provideaccount1
address as_receiver
forincreaseLock
function. The function will check that lock foraccount2
0's deposit is not expired yet. Then it will mint big amount of reward tokens(because of big lock amount of the deposit) and will add minted tokens and deposited tokens to the_receiver
same deposit with index 0(this is important to have deposits with same id). - Attcker somehow use minted tokens(to get rewards for example)
- Attacker from
account1
can withdraw all deposited tokens.
Impact
This mechanism can be used to deposit money just before reward distribution to get the share and then witdhraw deposited tokens.
It's possible to create a bot that will front run transactions when rewards are toped up to deposit big amount of stake tokens. Then after that get rewards and withdraw tokens. All this can be done in 1 block. The only condition for attacker is to have mock deposit account already expired, but not withdrawn.
Code Snippet
This code will show that this is possible
it.only("Increasing lock for another receiver will broke system", async() => {
await timeLockPool.deposit(DEPOSIT_AMOUNT, constants.MaxUint256, account1.address);
const startUserDepostit = await timeLockPool.depositsOf(account1.address, 0);
console.log("account 1 deposit amount: " + Number(startUserDepostit.amount._hex));
const startBalance = await timeLockPool.balanceOf(account1.address);
console.log("account 1 pool token balance: " + Number(startBalance._hex));
let startBalanceOfAnotherAccount = await timeLockPool.balanceOf(account2.address);
expect(startBalanceOfAnotherAccount).to.be.eq(0)
//change lock for account2
await timeLockPool.deposit(1, 0, account2.address);
await timeTraveler.increaseTime(60 * 10 * 2);
let anotherAccountDeposit = await timeLockPool.depositsOf(account2.address, 0);
console.log("account 2 deposit amount after first deposit: " + Number(anotherAccountDeposit.amount._hex));
let timelockTokenBalance = await timeLockPool.balanceOf(account2.address);
console.log("account 2 pool token balance after first deposit: " + Number(timelockTokenBalance._hex));
await timeLockPool.increaseLock(0, account2.address, INCREASE_AMOUNT);
anotherAccountDeposit = await timeLockPool.depositsOf(account2.address, 0);
console.log("account 2 deposit amount after increase lock: " + Number(anotherAccountDeposit.amount._hex));
timelockTokenBalance = await timeLockPool.balanceOf(account2.address);
console.log("account 2 pool token balance after increase lock: " + Number(timelockTokenBalance._hex));
await timeLockPool.connect(account2).withdraw(0, account2.address);
timelockTokenBalance = await timeLockPool.balanceOf(account2.address);
console.log("account 2 pool token balance after withdraw: " + Number(timelockTokenBalance._hex));
const depositTokenBalance = await depositToken.balanceOf(account2.address);
console.log("deposit token balance: " + depositTokenBalance);
});
Tool used
Manual Review
Recommendation
Check that lock of _receiver
has not expired yet. And also calculate multiplier for the _receiver
based on his deposit lock duration.
Duplicate of #102
8olidity - unit might be 0,The getMultiplier() function is not available
8olidity
medium
unit might be 0,The getMultiplier() function is not available
Summary
unit might be 0
Vulnerability Detail
Because of this
uint256 public constant MIN_LOCK_DURATION = 10 minutes;
if (_maxLockDuration < MIN_LOCK_DURATION) {
revert SmallMaxLockDuration();
}
maxLockDuration minimum is 600. If curve.length is greater than 600 on that day, unit will be 0 and getMultiplier() will calculate incorrectly
poc
// merit-liquidity-mining/test/TimeLockPool.ts
const MAX_LOCK_DURATION = 3;
it("Replacing with a same length curve should do it correctly", async() => {
// Mapping a new curve for replacing the old one
const NEW_CURVE = CURVE.map(function(x) {
return (hre.ethers.BigNumber.from(x).mul(2).toString())
})
await timeLockPool.connect(deployer).setCurve(NEW_CURVE);
console.log(await timeLockPool.maxLockDuration()); // console 3
console.log(await timeLockPool.unit()); // console 0
for(let i=0; i< NEW_CURVE.length; i++){
const curvePoint = await timeLockPool.curve(i);
expect(curvePoint).to.be.eq(NEW_CURVE[i])
}
await expect(timeLockPool.curve(NEW_CURVE.length + 1)).to.be.reverted;
})
Impact
The getMultiplier() function is not available
Code Snippet
function setCurve(uint256[] calldata _curve) external onlyGov {
if (_curve.length < 2) {
revert ShortCurveError();
}
// same length curves
if (curve.length == _curve.length) {
for (uint i=0; i < curve.length; i++) {
curve[i] = maxBonusError(_curve[i]);
}
// replacing with a shorter curve
} else if (curve.length > _curve.length) {
for (uint i=0; i < _curve.length; i++) {
curve[i] = maxBonusError(_curve[i]);
}
uint initialLength = curve.length;
for (uint j=0; j < initialLength - _curve.length; j++) {
curve.pop();
}
unit = maxLockDuration / (curve.length - 1);
// replacing with a longer curve
} else {
for (uint i=0; i < curve.length; i++) {
curve[i] = maxBonusError(_curve[i]);
}
uint initialLength = curve.length;
for (uint j=0; j < _curve.length - initialLength; j++) {
curve.push(maxBonusError(_curve[initialLength + j]));
}
unit = maxLockDuration / (curve.length - 1);
}
emit CurveChanged(_msgSender());
}
Tool used
vscode
Manual Review
Recommendation
Judge unit's value after you calculate it
carlitox477 - TimeLockPool#withdraw allows burning shares
carlitox477
medium
TimeLockPool#withdraw allows burning shares
Summary
This can be done by setting _receiver=address(0)
leading to lost of users deposit token
Vulnerability Detail
Calling withdraw with _receiver=address(0)
will allow msg.sender
to send their deposit token to address zero.
Impact
Allows users to burn their rewards
Code Snippet
Tool used
Manual Review
Recommendation
Add next require statement at the start of function withdraw: require(_receiver != address(0))
ctf_sec - The maxLockDuration parameter in TimeLockPool.sol does not have upper bound limit and does not conform to the 48 month maximum staking period business requirement.
ctf_sec
medium
The maxLockDuration parameter in TimeLockPool.sol does not have upper bound limit and does not conform to the 48 month maximum staking period business requirement.
Summary
The maxLockDuration parameter in TimeLockPool.sol does not have upper bound limit and does not conform to the 48 month maximum staking period business requirement.
Vulnerability Detail
the parameter maxLockDuration determines the maximum length of period that user can stake.
uint256 public maxLockDuration;
and
maxLockDuration = _maxLockDuration;
Then in deposit and this parameter is used to enforce the max lock period
In the function deposit
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
and in the function extendLock
// New duration is the time expiration plus the increase
uint256 duration = maxLockDuration.min(uint256(userDeposit.end - block.timestamp) + increaseDuration);
according to the documentation
Merit Circle Staking V2 is a module where holders of MC or MC/ETH LP tokens can lock their tokens up to 48 months to get rewards.
This business requirement is not implemented by adding a upper limit bound for the parameter maxLockDuration.
Impact
A Compromise admin can set the maxLockDuration to a very very very large number and stake a small amount token with a very large duration to collect reward.
The User is able to stake more than 48 month, which violates the business requirement.
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project add a upper limit bound for the parameter maxLockDuration
berndartmueller - Curve points are not validated to be continuously increasing
berndartmueller
medium
Curve points are not validated to be continuously increasing
Summary
Curve points are not validated to be continuously increasing and in case a curve point is less than its predecessor, the getMultiplier
function will revert
Vulnerability Detail
Curve points are set by governance in multiple places. Each point on the curve is expected to be greater than the previous point. However, this is not validated in the code. If a point is set to a value less than the previous point, the linear interpolation in TimeLockPool.getMultiplier
will revert due to curve[n + 1]
< curve[n]
.
Impact
The core functionality of the TimeLockPool
contract will be broken and will revert until the curve is fixed by governance.
Code Snippet
function getMultiplier(uint256 _lockDuration) public view returns(uint256) {
// There is no need to check _lockDuration amount, it is always checked before
// in the functions that call this function
// n is the time unit where the lockDuration stands
uint n = _lockDuration / unit;
// if last point no need to interpolate
// trim de curve if it exceedes the maxBonus // TODO check if this is needed
if (n == curve.length - 1) {
return 1e18 + curve[n];
}
// linear interpolation between points
return 1e18 + curve[n] + (_lockDuration - n * unit) * (curve[n + 1] - curve[n]) / unit; // @audit-info Can revert due to `curve[n + 1] - curve[n]`
}
Tool used
Manual review
Recommendation
Consider adding appropriate checks to all places where the curve points are set or changed to ensure that a curve point is not less than the previous point.
Duplicate of #111
Ruhum - Pool doesn't support fee-on-transfer tokens
Ruhum
medium
Pool doesn't support fee-on-transfer tokens
Summary
Some tokens take or might take a fee on each transfer in the future. A good example is USDT. The TimeLockPool contract doesn't take that into account. Any pool that uses such a token won't work as expected because of the wrong internal bookkeeping.
Vulnerability Detail
When a user calls deposit()
the contract expects to receive exactly amount
tokens: https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L94
If depositToken
is a fee-on-transfer token, the contract will instead receive x
where x < amount
. If the same user now calls withdraw()
, the transaction will fail because the contract doesn't have enough tokens to cover the withdrawal: https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L133
Impact
The contract will have fewer tokens than it expects to have. There will be no issues for initial withdrawals because the other user's deposits should cover their transactions. Later withdrawals will fail and the remaining funds will be locked inside the contract.
Code Snippet
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
// Enforce min lockup duration to prevent flash loan or MEV transaction ordering
duration = duration.max(MIN_LOCK_DURATION);
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
depositsOf[_receiver].push(Deposit({
amount: _amount,
shareAmount: mintAmount,
start: uint64(block.timestamp),
end: uint64(block.timestamp) + uint64(duration)
}));
_mint(_receiver, mintAmount);
emit Deposited(_amount, duration, _receiver, _msgSender());
}
Tool used
Manual Review
Recommendation
You can support fee-on-transfer tokens by verifying how many tokens you got from a transfer:
uint initialBalance = token.balanceOf(address(this));
token.safeTransferFrom(user, address(this), amount);
uint actualBalance = token.balanceOf(address(this)) - initialBalance;
berndartmueller - Withdrawing deposits within a batch transaction can lead to issues with deposit ids
berndartmueller
medium
Withdrawing deposits within a batch transaction can lead to issues with deposit ids
Summary
The deposit ids change whenever a deposit is withdrawn. This can cause issues when using deposit ids in batch transactions.
Vulnerability Detail
Deposit ids are based on the index of a deposit within the depositsOf[msg.sender]
array. On withdrawal, deposits are reordered - the last deposit from a user will be placed at the position of the withdrawn deposit with id _depositId
and the last deposit is removed. Hence the deposit id of the last deposit has changed. This can lead to issues when withdrawing deposits and, for example, extending deposit locks within a batch transaction. Then the change of deposit ids has to be taken into account by the user calling the batch transaction. Otherwise, if the deposit id of the previously last deposit is referenced, the transaction will revert with an out-of-bounds error.
Consider the following example:
- Alice has 10 deposits with ids
0
,1
,2
,3
,4
,5
,6
,7
,8
,9
. - Alice creates and executes the following batch transaction:
- Withdraw the deposit with id
6
- Withdraw the deposit with id
9
- Withdraw the deposit with id
- The batch transaction will revert due to no deposit exists with id
9
(the previous deposit with id9
has its deposit id changed to id6
)
Impact
Wrong and non-existent deposits are referenced by deposit ids, leading batch transactions to revert.
Code Snippet
function withdraw(uint256 _depositId, address _receiver) external {
if (_depositId >= depositsOf[_msgSender()].length) {
revert NonExistingDepositError();
}
Deposit memory userDeposit = depositsOf[_msgSender()][_depositId];
if (block.timestamp < userDeposit.end) {
revert TooSoonError();
}
// remove Deposit
depositsOf[_msgSender()][_depositId] = depositsOf[_msgSender()][depositsOf[_msgSender()].length - 1]; // @audit-info this will change deposit ids
depositsOf[_msgSender()].pop();
// burn pool shares
_burn(_msgSender(), userDeposit.shareAmount);
// return tokens
depositToken.safeTransfer(_receiver, userDeposit.amount);
emit Withdrawn(_depositId, _receiver, _msgSender(), userDeposit.amount);
}
Tool used
Manual review
Recommendation
Consider using immutable deposit ids instead of referencing the deposit as the index in the array.
Bnke0x0 - Ensure zero msg.value if transferring from user and input Token is not ETH
Bnke0x0
medium
Ensure zero msg.value if transferring from user and input Token is not ETH
Summary
Ensure zero msg.value if transferring from user and input Token is not ETH
Vulnerability Detail
Impact
A user that mistakenly calls either create() or addToken() with WETH (or another ERC20) as the input token, but includes native ETH with the function call will have his native ETH permanently locked in the contract.
Code Snippet
'depositToken.safeTransferFrom(_msgSender(), address(this), _amount);'
'depositToken.safeTransferFrom(_msgSender(), address(this), _increaseAmount);'
'rewardToken.safeTransferFrom(_msgSender(), address(this), _amount);'
Tool used
Manual Review
Recommendation
It is best to ensure that msg.value = 0
cccz - Too few tokens minted(too many tokens burned) in extendLock function
cccz
medium
Too few tokens minted(too many tokens burned) in extendLock function
Summary
Too few tokens minted(too many tokens burned) in extendLock function
Vulnerability Detail
When the user increases the lock duration in the extendLock function, the number of tokens minted is the number of tokens that should be minted in the new lock duration minus the number of tokens previously minted.
Consider the following scenario.
If the user locks 1000 deposited tokens, he will get 100 tokens in 2 months, 300 tokens in 3 months, and 800 tokens in 6 months.
If the user locks 1000 deposited tokens for 3 months, the user gets 300 tokens at that time.
After two months, the user decides to add another 5 months to the lock duration, so the new lock duration is 6 months and the user gets 800-300 = 500 tokens.
In total, the user has been locked in for 8 months, but has only received 800 tokens.
I think a better practice would be for the user to get 100 + 800 = 900 tokens.
Impact
The user gets less tokens than expected when calling the extendLock function
Code Snippet
Tool used
Manual Review
Recommendation
Consider adding the following gottenAmount to the calculation
gottenAmount = userDeposit.amount * getMultiplier((block.timestamp - userDeposit.start)) / 1e18;
if (mintAmount + gottenAmount > userDeposit.shareAmount) {
depositsOf[_msgSender()][_depositId].shareAmount = mintAmount;
_mint(_msgSender(), mintAmount + gottenAmount - userDeposit.shareAmount);
// If the new amount is less then burn that difference
} else if (mintAmount + gottenAmount < userDeposit.shareAmount) {
depositsOf[_msgSender()][_depositId].shareAmount = mintAmount;
_burn(_msgSender(), userDeposit.shareAmount - mintAmount - gottenAmount);
}
cccz - Consider adding minAmount to extendLock/increaseLock/deposit for slippage control
cccz
medium
Consider adding minAmount to extendLock/increaseLock/deposit for slippage control
Summary
Consider adding minAmount to extendLock/increaseLock/deposit for slippage control
Vulnerability Detail
There is no minimum limit to the number of tokens that are minted (or burned) in the extendLock/increaseLock/deposit functions, which may result in the user receiving less tokens than expected if the transactions calling these functions occur in the same block as the transactions calling the setCurvePoint/setCurve functions.
Impact
This may result in the user receiving less tokens than expected.
Code Snippet
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L85-L107
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L148-L179
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L197-L220
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L233-L246
Tool used
Manual Review
Recommendation
Consider adding minAmount to extendLock/increaseLock/deposit for slippage control
Duplicate of #113
0xmuxyz - There is possibility that leave a contract "uninitialized" and that contract will be used for malicious attack
0xmuxyz
high
There is possibility that leave a contract "uninitialized" and that contract will be used for malicious attack
Summary
- The vulnerability is leaving the implementation contract
uninitialized
, which allows a malicious attacker to callinitialize()
function to upgrade the implementation contract. That allow malicious attacker to upgrade malicious contract as a new implementation contract and steal fund locked in contract by executing bad functions.
Vulnerability Detail
- The vulnerability is leaving the implementation contract (ex. TimeLockNonTransferablePool.sol)
uninitialized
, which allows a malicious attacker to callinitialize()
function to upgrade the implementation contract. As a result, an attacker can insert any attack contract that includes bad functionalities into a parameter of new implementation contract. Then, once an attacker is successful to update the implementation contract, they can execute the bad functions.
Impact
- A malicious attacker can steal fund locked in contract by executing bad functions such as a function that withdraw all locked fund, etc.
Code Snippet
contract TimeLockNonTransferablePool is TimeLockPool {
function initialize(
string memory _name,
string memory _symbol,
address _depositToken,
address _rewardToken,
address _escrowPool,
uint256 _escrowPortion,
uint256 _escrowDuration,
uint256 _maxBonus,
uint256 _maxLockDuration,
uint256[] memory _curve
) public initializer {
__TimeLockPool_init(_name, _symbol, _depositToken, _rewardToken, _escrowPool, _escrowPortion, _escrowDuration, _maxBonus, _maxLockDuration, _curve);
}
Tool used
- Manual Review
Recommendation
- To avoid leaving a contract uninitialized, you should invoke the _disableInitializers() function in the constructor to automatically lock it when it is deployed:
- For example, in the TimeLockNonTransferablePool.sol, _disableInitializers() should be added with constructor like below.
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockNonTransferablePool.sol#L6-L20
contract TimeLockNonTransferablePool is TimeLockPool {
//@dev - _disableInitializers() should be added to avoid leaving a contract uninitialized.
constructor() {
_disableInitializers();
}
function initialize(
string memory _name,
string memory _symbol,
address _depositToken,
address _rewardToken,
address _escrowPool,
uint256 _escrowPortion,
uint256 _escrowDuration,
uint256 _maxBonus,
uint256 _maxLockDuration,
uint256[] memory _curve
) public initializer {
__TimeLockPool_init(_name, _symbol, _depositToken, _rewardToken, _escrowPool, _escrowPortion, _escrowDuration, _maxBonus, _maxLockDuration, _curve);
}
rvierdiiev - BasePool.distributeRewards function should be restricted to not be called by anyone
rvierdiiev
low
BasePool.distributeRewards function should be restricted to not be called by anyone
Summary
BasePool.distributeRewards
function can be misused by user, so he will lost his tokens.
Vulnerability Detail
Impact
User may lose funds.
Code Snippet
function distributeRewards(uint256 _amount) external override {
rewardToken.safeTransferFrom(_msgSender(), address(this), _amount);
_distributeRewards(_amount);
}
Tool used
Manual Review
Recommendation
Restrict this function to be called by government(onlyGov) or smth.
ctf_sec - Incompatability with deflationary / fee-on-transfer tokens
ctf_sec
medium
Incompatability with deflationary / fee-on-transfer tokens
Summary
When depositing and withdrawing, if the token is a fee-on-transfer tokens, the internal accounting will have issue.
Vulnerability Detail
when depositing, we call
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
We assume that the _amount will passed accurately transferred into the TimeLockPool.sol,
however, if the underlying token is fee-on-transfer token, for example, we transfer 100 token, but when transfering, the token charge a fee and 95 Token is what the contract receive,
the contract will still use the 100 balance to mint share, which is not accurate.
When withdraw, we call
// burn pool shares
_burn(_msgSender(), userDeposit.shareAmount);
// return tokens
depositToken.safeTransfer(_receiver, userDeposit.amount);
However, if the underlying token is rebasing, the TimeLockContract have more token balance then the user deposit, then the rebased token balance will be lost because we just withdraw userDeposit.amount
Impact
In Deposit, the amount of share minted is not accurate because the amount we intended to receive can be difficult from the amount we received.
In Withdraw, user may lose the rebasing amount of token.
Code Snippet
Tool used
Manual Review
Recommendation
Consider compare pre-/after token balances to compute the actual transferred amount.
berndartmueller - Unsafe cast of `pointsPerShare` can cause wrong reward calculation
berndartmueller
medium
Unsafe cast of pointsPerShare
can cause wrong reward calculation
Summary
The unsafe cast of pointsPerShare
from uint256
to int256
can cause less earned rewards than anticipated.
Vulnerability Detail
The unsafe cast of pointsPerShare
to int256
can cause its value to be different from the correct value. If the unsigned value of pointsPerShare
is above the maximum signed value type(int256).max
, it will be interpreted as a negative value instead. Then pointsCorrection[account]
will be incorrect and the amount of earned rewards for a user will be less than expected.
Impact
A user will earn fewer rewards than expected.
Code Snippet
/**
* @dev Increases or decreases the points correction for `account` by
* `shares*pointsPerShare`.
*/
function _correctPoints(address _account, int256 _shares) internal {
pointsCorrection[_account] = pointsCorrection[_account] + (_shares * (int256(pointsPerShare)));
}
Tool used
Manual review
Recommendation
Consider using SafeCast
to cast pointsPerShare
to int256
:
function _correctPoints(address _account, int256 _shares) internal {
pointsCorrection[_account] = pointsCorrection[_account] + (_shares * pointsPerShare.toInt256());
}
Duplicate of #59
rvierdiiev - Curve mess up is posssible that leads to deposit function blocked
rvierdiiev
medium
Curve mess up is posssible that leads to deposit function blocked
Summary
It's possible that the multiplier for deposited tokens will be calculated incorrectly when curves values are not validated and will be not possible to deposit.
Vulnerability Detail
When user deposits token then some multiplier is calculated to know the amount of share tokens to be minted. This multiplier is calculated here and it uses state variable curve
inside.
It's possible that curve values will be provided in wrong order, because there is no validation nor in the constructor nor in the setter that curve[i+1] > curve[i]
. There is only check that curve[i]
is not bigger than maxBonus
.
In case if curve[i+1] > curve[i]
then this line in multiplier calculation function will revert with underflow error.
So depositing is not possible anymore.
Impact
Calculation of share tokens can be broken.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Do validation of curve array elements, so curve[i+1] > curve[i]
.
Duplicate of #111
Ch_301 - Missing updating of the `unit` value.
Ch_301
high
Missing updating of the unit
value.
Summary
Missing updating of the unit
value.
Vulnerability Detail
On TimeLockPool.sol
== > setCurvePoint()
When the Gov
decide to add a point to the curve
or remove the last point of the curve
} else if (_position == curve.length) {
curve.push(_newPoint);
} else {
if (curve.length - 1 < 2) {
revert ShortCurveError();
}
curve.pop();
}
There is no updating to the unit
value
We can see here how the unit
is calculating
unit = maxLockDuration / (curve.length - 1);
So any change in the curve.length
will affect the unit
value which means getMultiplier()
will return the wrong value.
Impact
getMultiplier()
will deliver a wrong multiplier
value
And this will affect the share
s calculation on deposit()
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
Code Snippet
function setCurvePoint(uint256 _newPoint, uint256 _position) external onlyGov {
if (_newPoint > maxBonus) {
revert MaxBonusError();
}
if (_position < curve.length) {
curve[_position] = _newPoint;
} else if (_position == curve.length) {
curve.push(_newPoint);
} else {
if (curve.length - 1 < 2) {
revert ShortCurveError();
}
curve.pop();
}
emit CurveChanged(_msgSender());
}
Tool used
Manual Review
Recommendation
If you add or remove a point to/from the curve
Reculcale the unit
unit = maxLockDuration / (curve.length - 1);
Duplicate of #101
8olidity - Consider whether depositToken and rewardToken are deflationary tokens
8olidity
medium
Consider whether depositToken and rewardToken are deflationary tokens
Summary
Consider whether depositToken and rewardToken are deflationary tokens
Vulnerability Detail
If depositToken is a deflationary token, the timelockpool contract cannot get _amount of tokens after deposit()
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
// Enforce min lockup duration to prevent flash loan or MEV transaction ordering
duration = duration.max(MIN_LOCK_DURATION);
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
depositsOf[_receiver].push(Deposit({
amount: _amount,
shareAmount: mintAmount,
start: uint64(block.timestamp),
end: uint64(block.timestamp) + uint64(duration)
}));
_mint(_receiver, mintAmount);
emit Deposited(_amount, duration, _receiver, _msgSender());
}
If rewardToken is a deflationary token, distributeRewards() Basepool will also not get _amount of tokens
function distributeRewards(uint256 _amount) external override {
rewardToken.safeTransferFrom(_msgSender(), address(this), _amount);
_distributeRewards(_amount);
}
Impact
The token number is different from the actual number
Code Snippet
Tool used
vscode
Manual Review
Recommendation
Add the depositToken and rewardToken whitelists
Ch_301 - Missing checking `address(0)`
Ch_301
medium
Missing checking address(0)
Summary
Missing checking address(0)
Vulnerability Detail
On TimeLockPool.sol
==> deposit()
and increaseLock()
In case the user set _receiver == address(0)
by mistake
he will lose all their shares
And on withdraw()
also but here he will lose their funds, the pool will send the funds to the address(0)
.
Impact
The user could lose their shares
or funds
Code Snippet
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
// Enforce min lockup duration to prevent flash loan or MEV transaction ordering
duration = duration.max(MIN_LOCK_DURATION);
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
depositsOf[_receiver].push(Deposit({
amount: _amount,
shareAmount: mintAmount,
start: uint64(block.timestamp),
end: uint64(block.timestamp) + uint64(duration)
}));
_mint(_receiver, mintAmount);
emit Deposited(_amount, duration, _receiver, _msgSender());
}
function increaseLock(uint256 _depositId, address _receiver, uint256 _increaseAmount) external {
// Check if actually increasing
if (_increaseAmount == 0) {
revert ZeroAmountError();
}
Deposit memory userDeposit = depositsOf[_msgSender()][_depositId];
// Only can extend if it has not expired
if (block.timestamp >= userDeposit.end) {
revert DepositExpiredError();
}
depositToken.safeTransferFrom(_msgSender(), address(this), _increaseAmount);
// Multiplier should be acording the remaining time to the deposit to end
uint256 remainingDuration = uint256(userDeposit.end - block.timestamp);
uint256 mintAmount = _increaseAmount * getMultiplier(remainingDuration) / 1e18;
depositsOf[_receiver][_depositId].amount += _increaseAmount;
depositsOf[_receiver][_depositId].shareAmount += mintAmount;
_mint(_receiver, mintAmount);
emit LockIncreased(_depositId, _receiver, _msgSender(), _increaseAmount);
}
function withdraw(uint256 _depositId, address _receiver) external {
if (_depositId >= depositsOf[_msgSender()].length) {
revert NonExistingDepositError();
}
Deposit memory userDeposit = depositsOf[_msgSender()][_depositId];
if (block.timestamp < userDeposit.end) {
revert TooSoonError();
}
// remove Deposit
depositsOf[_msgSender()][_depositId] = depositsOf[_msgSender()][depositsOf[_msgSender()].length - 1];
depositsOf[_msgSender()].pop();
// burn pool shares
_burn(_msgSender(), userDeposit.shareAmount);
// return tokens
depositToken.safeTransfer(_receiver, userDeposit.amount);
emit Withdrawn(_depositId, _receiver, _msgSender(), userDeposit.amount);
}
Tool used
Manual Review
Recommendation
Add check if _receiver == address(0)
carlitox477 - TimeLockPool#deposit allow burning shares
carlitox477
medium
TimeLockPool#deposit allow burning shares
Summary
This can be done by setting _receiver=address(0)
leading to lost of users deposit token
Vulnerability Detail
Calling deposit with _receiver=address(0)
will allow msg.sender
to send their deposit token to address zero.
Impact
Allows users to burn their rewards
Code Snippet
Tool used
Manual Review
Recommendation
Add next require statement at the start of function deposit: require(_receiver != address(0))
apajaresaguilera - Prevent reentrancy
apajaresaguilera
low
Prevent reentrancy
Summary
A reentrancy attack can be performed if the reward token is updated to be an ERC777 token.
Impact
Low
Vulnerability Detail
A lack of reentrancy preventions allow users to reenter relevant protocol functions, such as claimRewards()
in BasePool.sol, or withdraw()
in TimeLockPool.sol .
If the reward token is updated to be an ERC777 token in the future (which has support for hooks), a malicious party could re-enter the function that performed the token transfer by calling the function transferring the tokens again in the ERC777 hook.
Especially in claimRewards()
, where users obtain the rewards corresponding to their staking amount, the transfer could call a callback triggering an attacker's malicious contract function where claimRewards()
could be called again.
Code Snippet
function claimRewards(address _receiver) external {
uint256 rewardAmount = _prepareCollect(_msgSender());
uint256 escrowedRewardAmount = rewardAmount * escrowPortion / 1e18;
uint256 nonEscrowedRewardAmount = rewardAmount - escrowedRewardAmount;
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
}
// ignore dust
if(nonEscrowedRewardAmount > 1) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
emit RewardsClaimed(_msgSender(), _receiver, escrowedRewardAmount, nonEscrowedRewardAmount);
}
Tool used
Manual Review
Recommendation
Add reentrancy guards to functions transferring reward tokens back from the protocol (claimRewards()
in BasePool.sol, or withdraw()
in TimeLockPool.sol) (see ReentrancyGuard.sol)
Lambda - increaseLock can be used to circumvent MIN_LOCK_DURATION
Lambda
medium
increaseLock can be used to circumvent MIN_LOCK_DURATION
Summary
The increaseLock
function can be used to circumvent the minimum lock duration that is normally enforced.
Vulnerability Detail
Normally, all locks need to have a duration that is greater than MIN_LOCK_DURATION
. However, a user can call increaseLock
just before the end time to essentially have locks with arbitrary small times. On the first deposit (where the duration is enforced), he could only deposit 1 wei and then increase it by 10^20 wei just before the end.
Impact
This circumvents a security measure of the system and allows attackers to gain a significant share with very short-term loans, which should not be possible. For instance, with a loan of just 12 seconds (1 block), someone could mint the majority of the tokens for him.
Code Snippet
Tool used
Manual Review
Recommendation
Also enforce MIN_LOCK_DURATION
when increasing a lock.
Duplicate of #67
gatsbyjr - Unlimited minting and burning is possible through TestBasePool.sol
gatsbyjr
high
Unlimited minting and burning is possible through TestBasePool.sol
Summary
External Contracts can directly call BasePool.mint() through TestBasePool.sol.
Vulnerability Detail
TestBasePool.sol#L40-46
function mint(address _receiver, uint256 _amount) external {
_mint(_receiver, _amount);
}
function burn(address _from, uint256 _amount) external {
_burn(_from, _amount);
}
TestBasePool inherits BasePool.sol without an auth check, allowing an external contract to directly call _mint and _burn without having to deposit any tokens.
Impact
A malicious user can arbitrarily mint shares as well as burn users' shares causing economic loss
Code Snippet
Tool used
Manual Review
Recommendation
Add an onlyOwner modifier.
Bnke0x0 - Low level call returns true if the address doesn't exist
Bnke0x0
medium
Low level call returns true if the address doesn't exist
Summary
As written in the solidity documentation, the low-level functions call, delegatecall and staticcall return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed.
Vulnerability Detail
Impact
The low-level function delegatecall are used in some places in the code and it can be problematic.
Code Snippet
' function batch(bytes[] calldata calls, bool revertOnFail) external payable {
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(calls[i]);
if (!success && revertOnFail) {
revert(_getRevertMsg(result));
}'
Tool used
Manual Review
Recommendation
Check before any low-level call that the address actually exists, for example before the low-level call in the callERC20 function you can check that the address is a contract by checking its code size.
ctf_sec - A large amount of reward will be stucked in the TimeLockPool.sol if _escrowPool is address(0) and not set up.
ctf_sec
high
A large amount of reward will be stucked in the TimeLockPool.sol if _escrowPool is address(0) and not set up.
Summary
A large amount of reward can be stucked in the TimeLockPool.sol if _escrowPool is not set up.
Vulnerability Detail
When claiming reward, the code deposit a portion of the reward to _escrowPool, which is another TimeLockPool
uint256 rewardAmount = _prepareCollect(_msgSender());
uint256 escrowedRewardAmount = rewardAmount * escrowPortion / 1e18;
uint256 nonEscrowedRewardAmount = rewardAmount - escrowedRewardAmount;
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
}
// ignore dust
if(nonEscrowedRewardAmount > 1) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
However, if the escrowPool is not set up, the escrowedRewardAmount will be stucked in the contract and not claimable.
Impact
if address(escrowPool) is address(0), the code below will never run
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
}
then the reward amount "escrowedRewardAmount" will be stucked
Code Snippet
Tool used
Manual Review
Recommendation
We recommend refractor the code to make sure if the _escrowPool is not set, transfer all the reward to receiver.
function claimRewards(address _receiver) external {
uint256 rewardAmount = _prepareCollect(_msgSender());
uint256 escrowedRewardAmount = rewardAmount * escrowPortion / 1e18;
uint256 nonEscrowedRewardAmount = rewardAmount - escrowedRewardAmount;
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
} else {
nonEscrowedRewardAmount += escrowedRewardAmount; // here.
}
// ignore dust
if(nonEscrowedRewardAmount > 1) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
emit RewardsClaimed(_msgSender(), _receiver, escrowedRewardAmount, nonEscrowedRewardAmount);
}
Duplicate of #107
carlitox477 - BasePool#claimRewards allows users to burn rewards
carlitox477
medium
BasePool#claimRewards allows users to burn rewards
Summary
This can be done by setting _receiver=address(0)
leading to lost of users rewards
Vulnerability Detail
Calling claimRewards(address(0))
will allow msg.sender
to burn their rewards.
Impact
Allows users to burn their rewards
Code Snippet
Tool used
Manual Review
Recommendation
Add next require statement at the start of function claimRewards: require(_receiver != address(0))
Bnke0x0 - Collect modules can fail on zero amount transfers if depositToken and rewardToken fee is set to zero
Bnke0x0
medium
Collect modules can fail on zero amount transfers if depositToken and rewardToken fee is set to zero
Summary
Some ERC20 tokens revert on zero value transfers:
https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers
Vulnerability Detail
Impact
A user that mistakenly calls either create() or addToken() with WETH (or another ERC20) as the input token, but includes native ETH with the function call will have his native ETH permanently locked in the contract.
Code Snippet
'depositToken.safeTransferFrom(_msgSender(), address(this), _amount);'
'depositToken.safeTransferFrom(_msgSender(), address(this), _increaseAmount);'
'rewardToken.safeTransferFrom(_msgSender(), address(this), _amount);'
Tool used
Manual Review
Recommendation
Consider checking the depositToken and rewardToken fee amount and do transfer only when it is positive.
Zarf - Ability to enjoy max benefits while only being locked for the minimum duration
Zarf
high
Ability to enjoy max benefits while only being locked for the minimum duration
Summary
The increaseLock()
function of the TimeLockPool
is using the wrong deposit to verify whether the deposit has been ended and to calculate the remaining duration.
Vulnerability Detail
The increaseLock()
function of the TimeLockPool
contract verifies the remaining duration and whether the deposit has been expired on the depositId linked to the msg.sender. While on the other hand, the amount of deposit tokens and share amount is increased on the depositId linked to the receiver.
Impact
An attacker could exploit the vulnerability as follows:
- Create a new lock A with a very small amount and
maxLockDuration
from account A - Create a new lock B (or use an existing one) with an arbitrary amount and
MIN_LOCK_DURATION
(10 minutes) from account B. - Ensure the
depositId
of both locks are the same. This could be done by creating/deleting arbitrary locks until the amount of deposit’s of account A and B are the same. - Call the
increaseLock
function from the account linked to lock A with a large_increaseAmount
and address of account B as_receiver
- Lock B (with the small lock duration) will be increased with the
_increaseAmount
and a bonus of shares multiplied by 5 (due to themaxLockDuration
of lock A). - The attacker benefits from the 500% multiplier benefits while only having to lock their assets for a minimum of 10 minutes.
Code Snippet
Tool used
Manual Review
Recommendation
Assign the userDeposit
to the _depositId
linked to the _receiver
instead of _msgSender()
as follows:
Deposit memory userDeposit = depositsOf[_receiver][_depositId];
Duplicate of #102
Chom - if (nonEscrowedRewardAmount > 1) is not correct
Chom
medium
if (nonEscrowedRewardAmount > 1) is not correct
Summary
if (nonEscrowedRewardAmount > 1) is not correct
Vulnerability Detail
// ignore dust
if(nonEscrowedRewardAmount > 1) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
rewardToken must be transferred if nonEscrowedRewardAmount > 0 but currently transferred if nonEscrowedRewardAmount > 1.
Impact
If nonEscrowedRewardAmount == 1, rewardToken won't transfer on reward claiming
Code Snippet
Tool used
Manual Review
Recommendation
if (nonEscrowedRewardAmount > 0)
// ignore dust
if(nonEscrowedRewardAmount > 0) {
rewardToken.safeTransfer(_receiver, nonEscrowedRewardAmount);
}
defsec - Persisted msg.value in a loop of delegate calls can be used to drain ETH from your proxy
defsec
high
Persisted msg.value in a loop of delegate calls can be used to drain ETH from your proxy
Summary
msg.value in a loop can be used to drain proxy funds.
Vulnerability Detail
While BoringBatchable is out of the scope, this bug affects seriously BasePool.sol#L17 as it inherits.
This vulnerability comes from the fact that msg.value
and msg.sender are persisted in delegatecall
.
It is possible to call execute()
(which is payable ) from batch()
(which is also payable ) because both are public functions. (For now ignore the fact that execute()
has access control).
The attacker would call batch()
sending, for example, 1 ETH with an array of 100 equal items that call execute()
This execute()
will call and external contract 100 times and in every time it will send 1ETH from proxy funds (not from the attacker).
If the receiving contract stores these value then the proxy wallet will be drained.
Impact
msg.value in a loop can be used to drain proxy funds.
Code Snippet
While this is already a high risk and there should be many attacking scenarios I would like to show you a pretty simple one.
Suppose the owner would like to grant access to a target with a normal function (maybe no even payable).
For example suppose that the owner grant access to the function
function goodFunction() public
This function has the selector 0x0d092393
. However, for some reason. the owner mistyped the selector and grant access to non existing function 0x0d09392
.
Then if the target contract has the so common function.
fallback() external payable { }
Then the attacker can drain wallet funds using this selector as I explained above.
Tool used
Manual Review
Recommendation
Remove payable
from batch()
in BoringBatchable
.
carlitox477 - AbstractRewards#_correctPoints can lead to wrong calculation due to unsafe casting
carlitox477
medium
AbstractRewards#_correctPoints can lead to wrong calculation due to unsafe casting
Summary
Unsafe casting of state variable pointsPerShare
if pointsPerShare > 2^255
Vulnerability Detail
Key dependant functions affected:
Impact
if pointsPerShare > 2^255
then pointsCorrection[_account]
will not be updated.
Code Snippet
Tool used
Manual Review
Recommendation
Mitigation steps:
Replace this line for pointsCorrection[_account] = pointsCorrection[_account] + (_shares * (pointsPerShare.toInt256()));
using the already imported Open Zeppelin library SafeCast
Duplicate of #59
carlitox477 - BasePool#__BasePool_init does not check some parameters
carlitox477
medium
BasePool#__BasePool_init does not check some parameters
Summary
BasePool#__BasePool_init does not check _rewardToken
and _escrowPool
to be different from zero address.
Vulnerability Detail
The code allows setting _rewardToken
and _escrowPool
to address zero without reverting, which might conduct to a wrong deployment, without way of changing both state variables value.
Impact
Setting the value of _rewardToken
and _escrowPool
to zero address would need to redeploy the contract
Code Snippet
Tool used
Manual Review
Recommendation
Add next two require statements to __BasePool_init function:
require(_rewardToken!=address(0));
require(_escrowPool!=address(0));
rvierdiiev - Protocol doesn't work proper with fee-on-transfer tokens
rvierdiiev
medium
Protocol doesn't work proper with fee-on-transfer tokens
Summary
When fee-on-transfer tokens will be used for staking then protocol will lost some funds.
Vulnerability Detail
Some ERC20 tokens may take fee when transfer assets.
This is how deposit
function works.
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
// Enforce min lockup duration to prevent flash loan or MEV transaction ordering
duration = duration.max(MIN_LOCK_DURATION);
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
depositsOf[_receiver].push(Deposit({
amount: _amount,
shareAmount: mintAmount,
start: uint64(block.timestamp),
end: uint64(block.timestamp) + uint64(duration)
}));
_mint(_receiver, mintAmount);
emit Deposited(_amount, duration, _receiver, _msgSender());
}
After token transfering it doesn't check how the balance increased. Provided by user _amount
param is saved to the Deposit
struct. Also share amount that is minted depends on _amount
param.
The same is for increaseLock
function.
When user withdraw then saved amount is transferred back.
Impact
Protocol calculates more deposited tokens for the depositor. When people will start withdrawing their staked funds some of them can be left without some part. Also protocol mints a bigger share for the depositor.
Code Snippet
Provided above.
Tool used
Manual Review
Recommendation
Check balance before and after sending tokens to the pool. Use the difference as provided amount.
ctf_sec - Give _escrowPool max allowance is risky in BasePool.sol#__BasePool_init
ctf_sec
medium
Give _escrowPool max allowance is risky in BasePool.sol#__BasePool_init
Summary
Give _escrowPool max allowance is risky in BasePool.sol#__BasePool_init
Vulnerability Detail
Give _escrowPool max allowance is risky in BasePool.sol#__BasePool_init
if(_rewardToken != address(0) && _escrowPool != address(0)) {
IERC20(_rewardToken).safeApprove(_escrowPool, type(uint256).max);
}
Given that _escrowPool is another upgradeable TimeLockPool, if the underlying smart contract is compromised, the malicious contract can drain the fund from the original TimeLockPool.
Source: https://kalis.me/unlimited-erc20-allowances/
Why are unlimited ERC20 allowances harmful?
When depositing a specific amount (say 100 DAI) into a contract, you can choose to set an allowance of exactly that amount. But instead, many apps instead request an unlimited allowance from the user.
This offers a superior user experience because the user does not need to approve a new allowance every time they want to deposit tokens. By setting up an unlimited allowance, the user just needs to approve it once, and not repeat the process for subsequent deposits.
However, this setup comes with significant drawbacks. As we know, bugs can exist and exploits can happen even in established projects. And by giving these platforms an unlimited allowance, you do not only expose your deposited funds to these risks, but also the tokens that you're holding "safely" in your wallet.
Impact
if the underlying smart contract is compromised, the malicious contract can drain the fund from the original TimeLockPool.
Recently, a lot user suffers from TRANSIT SWAP hack because the TRANSIT SWAP contract is compromised and any user that does not revoke approval lose their fund
Source: https://rekt.news/transit-swap-rekt/
Code Snippet
Tool used
Manual Review
Recommendation
We recommend the project only give a least of allowance needed for the underlying contract to do the job
function claimRewards(address _receiver) external {
uint256 rewardAmount = _prepareCollect(_msgSender());
uint256 escrowedRewardAmount = rewardAmount * escrowPortion / 1e18;
uint256 nonEscrowedRewardAmount = rewardAmount - escrowedRewardAmount;
if(escrowedRewardAmount != 0 && address(escrowPool) != address(0)) {
IERC20(_rewardToken).safeApprove(_escrowPool, escrowedRewardAmount); // allowance is given here
escrowPool.deposit(escrowedRewardAmount, escrowDuration, _receiver);
}
Chom - cumulativeRewardsOf logic is incorrect
Chom
high
cumulativeRewardsOf logic is incorrect
Summary
cumulativeRewardsOf logic is incorrect
Vulnerability Detail
function cumulativeRewardsOf(address _account) public view override returns (uint256) {
return ((pointsPerShare * getSharesOf(_account)).toInt256() + pointsCorrection[_account]).toUint256() / POINTS_MULTIPLIER;
}
pointsPerShare is first multiplied with getSharesOf(_account) then add it with pointsCorrection[_account]
But pointsCorrection[_account] is a correction of getSharesOf(_account), it should be added first to get net share to calculate the reward.
Impact
cumulativeRewardsOf is completely wrong. Causing the reward system to be broken. Fund loss is happening anytime.
Code Snippet
Tool used
Manual Review
Recommendation
- getSharesOf(_account).toInt256() + pointsCorrection[_account] first
- Multiply with pointsPerShare
- Divide with POINTS_MULTIPLIER
function cumulativeRewardsOf(address _account) public view override returns (uint256) {
return (pointsPerShare * (getSharesOf(_account).toInt256() + pointsCorrection[_account])).toUint256() / POINTS_MULTIPLIER;
}
csanuragjain - User loss previous amount
csanuragjain
high
User loss previous amount
Summary
On extending the lock, user will lose all the amount which he has gathered before calling the extendLock function. This is happening due to an incorrect logic
Vulnerability Detail
- User has deposited amount 200 for duration 20 seconds for which mint amount was 100
- Post 10 seconds user decides to use extendLock function to extend by further 5 seconds
- This makes the duration as below
uint256 duration = maxLockDuration.min(uint256(userDeposit.end - block.timestamp) + increaseDuration);
// duration = maxLockDuration.min(20-10)+5)=15
uint256 mintAmount = userDeposit.amount * getMultiplier(duration) / 1e18;
// 200*getMultiplier(15)/1e18 = 60
depositsOf[_msgSender()][_depositId].shareAmount = mintAmount;
//depositsOf[_msgSender()][_depositId].shareAmount = 60
- So new mint Amount came become 60 and old was 100. But this mint Amount was calculated for last 15 seconds which does not care of mint Amount from initial 10 seconds. The mint Amount of 100-60=40 which user for for the past 10 seconds should be withdrawn to user account
Impact
User will lose funds
Code Snippet
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L165
https://github.com/sherlock-audit/2022-10-merit-circle/blob/main/merit-liquidity-mining/contracts/TimeLockPool.sol#L148
Tool used
Manual Review
Recommendation
Revise the calculation as shown (or simply withdraw the difference mint amount):
uint256 duration = maxLockDuration.min(increaseDuration);
...
if (mintAmount > userDeposit.shareAmount) {
depositsOf[_msgSender()][_depositId].shareAmount+ = mintAmount;
_mint(_msgSender(), mintAmount);
}
...
defsec - Front-runnable Initializers
defsec
medium
Front-runnable Initializers
Summary
All contract initializers were missing access controls, allowing any user to initialize the contract. By front-running the contract deployers to initialize the contract, the incorrect parameters may be supplied, leaving the contract needing to be redeployed.
Vulnerability Detail
All contract initializers were missing access controls, allowing any user to initialize the contract. By front-running the contract deployers to initialize the contract, the incorrect parameters may be supplied, leaving the contract needing to be redeployed.
Impact
Front-running the contracts
Code Snippet
Tool used
Code Review
Manual Review
Recommendation
While the code that can be run in contract constructors is limited, setting the owner in the contract's constructor to the msg.sender
and adding the onlyOwner
modifier to all initializers would be a sufficient level of access control.
ali_shehab - Current implementation does not support DOESN’T SUPPORT FEE ON TRANSFER TOKENS
ali_shehab
medium
Current implementation does not support DOESN’T SUPPORT FEE ON TRANSFER TOKENS
Summary
Some ERC20 tokens are implemented so a fee is taken when transferring them, for example STA
and PAXG
. The current implementation of the deposit
and withdraw
will mess up the accounting of the deposited amounts if the token will be a token like that, it will lead to a state where users won’t be able to receive their funds.
Vulnerability Detail
The amount is directly added to the struct deposit. However, if for example, the STA token burns 1% of the value provided to the transfer function, so this is what will happen.
- UserA will call deposit with the amount of 100.
- The contract will receive 99, but the amount added to the struct will be 100.
- When the user now calls the withdraw functions, it will go again to the
struct Deposit
and transfer the amount. - This might succeed due to other users locking too, so the transferred tokens will be taken from “their tokens”, but in the end there will be users left without an option to withdraw their funds, because the balance of the contract will be less than the locked amount that the contract is trying to transfer.
Impact
Code Snippet
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
// Enforce min lockup duration to prevent flash loan or MEV transaction ordering
duration = duration.max(MIN_LOCK_DURATION);
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
depositsOf[_receiver].push(Deposit({
amount: _amount,
shareAmount: mintAmount,
start: uint64(block.timestamp),
end: uint64(block.timestamp) + uint64(duration)
}));
_mint(_receiver, mintAmount);
emit Deposited(_amount, duration, _receiver, _msgSender());
}
function withdraw(uint256 _depositId, address _receiver) external {
if (_depositId >= depositsOf[_msgSender()].length) {
revert NonExistingDepositError();
}
Deposit memory userDeposit = depositsOf[_msgSender()][_depositId];
if (block.timestamp < userDeposit.end) {
revert TooSoonError();
}
// remove Deposit
depositsOf[_msgSender()][_depositId] = depositsOf[_msgSender()][depositsOf[_msgSender()].length - 1];
depositsOf[_msgSender()].pop();
// burn pool shares
_burn(_msgSender(), userDeposit.shareAmount);
// return tokens
depositToken.safeTransfer(_receiver, userDeposit.amount);
emit Withdrawn(_depositId, _receiver, _msgSender(), userDeposit.amount);
}
Tool used
Manual Review
Recommendation
Calculate the amount to add to the locked amount by the difference between the balances before and after the transfer instead of using the supplied value.
defsec - Incompatibility With Rebasing/Deflationary/Inflationary tokens
defsec
medium
Incompatibility With Rebasing/Deflationary/Inflationary tokens
Summary
The Merit Circle protocol do not appear to support rebasing/deflationary/inflationary tokens whose balance changes during transfers or over time. The necessary checks include at least verifying the amount of tokens transferred to contracts before and after the actual transfer to infer any fees/interest.
Vulnerability Detail
The Merit Circle protocol do not appear to support rebasing/deflationary/inflationary tokens whose balance changes during transfers or over time. The necessary checks include at least verifying the amount of tokens transferred to contracts before and after the actual transfer to infer any fees/interest.
Suppose 100 USDT is transferred via safeTransferFrom()
to the TimeLockPool contract.
And a fee is applied (currently 0, but might be changed in the future).
Then you might receive 99.99 USDT
Now you try to do _mint(_receiver, mintAmount); ( 100 USDT ),
minting amount will be less than excepted.
Impact
The internal accounting system of the liquidity would be inaccurate or break, affecting the protocol operation.
Code Snippet
function deposit(uint256 _amount, uint256 _duration, address _receiver) external override {
if (_amount == 0) {
revert ZeroAmountError();
}
// Don't allow locking > maxLockDuration
uint256 duration = _duration.min(maxLockDuration);
// Enforce min lockup duration to prevent flash loan or MEV transaction ordering
duration = duration.max(MIN_LOCK_DURATION);
depositToken.safeTransferFrom(_msgSender(), address(this), _amount);
uint256 mintAmount = _amount * getMultiplier(duration) / 1e18;
depositsOf[_receiver].push(Deposit({
amount: _amount,
shareAmount: mintAmount,
start: uint64(block.timestamp),
end: uint64(block.timestamp) + uint64(duration)
}));
_mint(_receiver, mintAmount);
emit Deposited(_amount, duration, _receiver, _msgSender());
}
Tool used
Manual Review
Recommendation
Determine the transferred amount by subtracting the before & after balance. Have a procedure to don't allow the use of rebasing/inflation/deflation underlying tokens.
JohnSmith - Unsafe cast on point correction calculations
JohnSmith
medium
Unsafe cast on point correction calculations
Summary
Unsafe cast may lead to wrong results.
Vulnerability Detail
Every time an accaunt's shares are minted or burned, there is a point correction calculation for the account, which involves uint256 pointsPerShare
. To do the needed multiplication shares * pointsPerShare, we have to cast all operands to same type.
In function _correctPoints()
shares represented as int256 _shares
.
So we need to cast uint256 pointsPerShare
to int256
.
The way it is done: int256(pointsPerShare)
will lead to problems when pointsPerShare
become > type(int256).max
Impact
Unsafe cast will lead pointsPerShare
to have some negative value in scope of this calculation, which will lead to wrong value for pointsCorrection[_account]
to be stored, and as result wrong amount of rewards distributed to the account.
Code Snippet
merit-liquidity-mining/contracts/base/AbstractRewards.sol
125: function _correctPoints(address _account, int256 _shares) internal {
126: pointsCorrection[_account] = pointsCorrection[_account] + (_shares * (int256(pointsPerShare)));
127: }
Tool used
Manual Review
Recommendation
Use SafeCast
library to safely cast the uint256
to int256
function _correctPoints(address _account, int256 _shares) internal {
- pointsCorrection[_account] = pointsCorrection[_account] + (_shares * (int256(pointsPerShare)));
+ pointsCorrection[_account] = pointsCorrection[_account] + (_shares * pointsPerShare.toInt256());
}
Duplicate of #59
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.