Project Name | loxodrome |
---|---|
Addresses | MasterChef 0x71A2Ae8E6E3E70643F60cD49a951206905232Ed5 |
MinterV2 0xCB92D1173FbB4391138408E81f7ccBAaaD71Bc19 |
|
RewardsDistributor 0x255d1342a448c947304486d0adb4054cf18dF32e |
|
Network | IoTeX |
Language | Solidity |
Code | ‣ |
A checkpoint can contain duplicated veNFTs (tokenIDs) under certain circumstances leading to double counting of voting balance. Malicious users could exploit this vulnerability to inflate the voting balance of their accounts and participate in governance and gauge weight voting, potentially causing loss of assets or rewards for other users if the inflated voting balance is used in a malicious manner (e.g. redirect rewards to gauges where attackers have a vested interest).
Following is the high-level pseudo-code of the existing _moveTokenDelegates
function, which is crucial for understanding the issue.
_findWhatCheckpointToWrite
function to return the index of a new checkpointfunction _moveTokenDelegates(
address srcRep,
address dstRep,
uint _tokenId
) internal {
.....
uint32 nextSrcRepNum = _findWhatCheckpointToWrite(srcRep);
uint[] storage srcRepNew = checkpoints[srcRep][nextSrcRepNum].tokenIds
However, the problem is that the _findWhatCheckpointToWrite function does not always return the index of a new checkpoint . It will return the last checkpoint if it has already been written once within the same block.
function _findWhatCheckpointToWrite(address account)
internal
view
returns (uint32)
{
uint _timestamp = block.timestamp;
uint32 _nCheckPoints = numCheckpoints[account];
if (
_nCheckPoints > 0 &&
checkpoints[account][_nCheckPoints - 1].timestamp == _timestamp
) {
return _nCheckPoints - 1;
} else {
return _nCheckPoints;
}
}
If someone triggers the _moveTokenDelegates more than once within the same block (e.g. perform NFT transfer twice to the same person), the _findWhatCheckpointToWrite function will return a new checkpoint in the first transfer but will return the last/previous checkpoint in the second transfer. This will cause the move token delegate logic to be off during the second transfer
Recommendation:
Follow https://github.com/velodrome-finance/contracts/commit/a670bfb62c69fe38ac918b734a03b68a5505f8a2#diff-f9c514e7e623e2b874104366a804a87afa0048ad5d7fda1c0d45fbfa7e3941b9
LoxoHolders::buy
has the same price for all modesIn the LoxoHolders
contract, the buy
function is used to handle NFT purchases for different sale statuses (Private Sale, WhiteList Sale, and Public Sale). However, the function uses the same NFT_PRICE
variable to check the amount of Ether sent for all sale statuses. Since each sale status has a different NFT price (0.295 ETH for Private Sale, 0.325 ETH for WhiteList Sale, and 0.35 ETH for Public Sale), it is not possible to distinguish which sale status the user is participating in when the prices are the same.
// contracts/LoxoHolders.sol
function buy(uint256 amount, bytes32[] memory proof) public payable {
require(presale != Status.Stop, 'Presale isnt available at this moment');
if(presale == Status.PrivateSale){
// Private mint price: 0.295 ETH and 5 NFTs per wallet
require(NFT_PRICE.mul(amount) == msg.value, "ETH value sent is not correct");
require(privateSale[msg.sender].add(amount) <= MAX_PER_MINT, "exceed maximum amount");
privateSale[msg.sender] = privateSale[msg.sender].add(amount);
originalMinters[msg.sender] = originalMinters[msg.sender].add(amount);
_mintTo(msg.sender, amount);
}if(presale == Status.WhiteListSale){
// Whitelisted mint price: 0.325 ETH and 10 NFTs per wallet
require(verifyLeaf(proof, msg.sender), "Not whitelisted.");
require(NFT_PRICE.mul(amount) == msg.value, "ETH value sent is not correct");
require(whiteListSale[msg.sender].add(amount) <= MAX_PER_MINT, "exceed maximum amount");
whiteListSale[msg.sender] = whiteListSale[msg.sender].add(amount);
originalMinters[msg.sender] = originalMinters[msg.sender].add(amount);
_mintTo(msg.sender, amount);
}else if(presale == Status.PublicSale){
// Public mint price: 0.35 ETH and 10 NFTs per wallet
require(NFT_PRICE.mul(amount) == msg.value, "ETH value sent is not correct");
require(publicSale[msg.sender].add(amount) <= MAX_PER_MINT, "exceed maximum amount");
publicSale[msg.sender] = publicSale[msg.sender].add(amount);
originalMinters[msg.sender] = originalMinters[msg.sender].add(amount);
_mintTo(msg.sender, amount);
}
}
Recommendation:
In the buy
function, set different prices based on the current presale
status instead of using the same NFT_PRICE
variable.