Summary
callReward
function does not properly calculate the amountRewards
of the HealthToken
based on the MartenitsaTokens
of users
at that time. It should allow users to get 1 HelthToken
every 3 new MartenitsaTokens
but it does not take into account that the CountMartenitsaTokensOwner
can change making or receiving MartenitsaTokens
using the makePresent
function.
Vulnerability Details
The calculation of the amountRewards
in the codebase does not differentiate the new MartenitsaTokens
from the old ones.
uint256 amountRewards = (count / requiredMartenitsaTokens) - _collectedRewards[msg.sender];
if (amountRewards > 0) {
_collectedRewards[msg.sender] = amountRewards;
healthToken.distributeHealthToken(msg.sender, amountRewards);
}
Impact
Considering a situation in which:
Bob buys 6 MartenitsaTokens
and got 2 HealthTokens’ using the
callRewardfunction. Then using the
makePresentfunction, he transfers 3
MartenisaTokensto Jack, and receives using the same function, 3 other new
MartenisaTokensfrom Sam. Then Bob should be able to use again the function
callRewardto get 1 more
HealthTokenfor the 3 new
MartenisaTokens` but he can not get any additional token.
Run the test using command: forge test --match-path test/MartenitsaMarketplace.t.sol --match-test testReward -vvvv
function testReward() public listMartenitsa {
vm.startPrank(jack);
martenitsaToken.createMartenitsa("good1");
martenitsaToken.createMartenitsa("good2");
martenitsaToken.createMartenitsa("good3");
martenitsaToken.createMartenitsa("good4");
martenitsaToken.createMartenitsa("good5");
marketplace.listMartenitsaForSale(1, 1 wei);
marketplace.listMartenitsaForSale(2, 1 wei);
marketplace.listMartenitsaForSale(3, 1 wei);
marketplace.listMartenitsaForSale(4, 1 wei);
marketplace.listMartenitsaForSale(5, 1 wei);
martenitsaToken.setApprovalForAll(address(marketplace), true);
vm.stopPrank();
vm.startPrank(chasy);
martenitsaToken.createMartenitsa("good6");
martenitsaToken.createMartenitsa("good7");
martenitsaToken.createMartenitsa("good8");
marketplace.listMartenitsaForSale(6, 1 wei);
marketplace.listMartenitsaForSale(7, 1 wei);
marketplace.listMartenitsaForSale(8, 1 wei);
martenitsaToken.setApprovalForAll(address(marketplace), true);
vm.stopPrank();
vm.startPrank(bob);
marketplace.buyMartenitsa{value: 1 wei}(0);
marketplace.buyMartenitsa{value: 1 wei}(1);
marketplace.buyMartenitsa{value: 1 wei}(2);
marketplace.buyMartenitsa{value: 1 wei}(3);
marketplace.buyMartenitsa{value: 1 wei}(4);
marketplace.buyMartenitsa{value: 1 wei}(5);
marketplace.collectReward();
martenitsaToken.setApprovalForAll(address(marketplace), true);
marketplace.makePresent(jack, 3);
marketplace.makePresent(jack, 4);
marketplace.makePresent(jack, 5);
vm.stopPrank();
vm.startPrank(sam);
marketplace.buyMartenitsa{value: 1 wei}(6);
marketplace.buyMartenitsa{value: 1 wei}(7);
marketplace.buyMartenitsa{value: 1 wei}(8);
marketplace.collectReward();
martenitsaToken.setApprovalForAll(address(marketplace), true);
marketplace.makePresent(bob, 6);
marketplace.makePresent(bob, 7);
marketplace.makePresent(bob, 8);
vm.stopPrank();
vm.startPrank(bob);
marketplace.collectReward();
assert(healthToken.balanceOf(bob) == 2 * 10 ** 18);
vm.stopPrank();
}
and you get the following result:
├─ [0] VM::stopPrank()
│ └─ ← ()
├─ [0] VM::startPrank(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ └─ ← ()
├─ [2889] MartenitsaMarketplace::collectReward()
│ ├─ [630] MartenitsaToken::isProducer(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
│ │ └─ ← false
│ ├─ [627] MartenitsaToken::getCountMartenitsaTokensOwner(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
│ │ └─ ← 6
│ └─ ← ()
├─ [629] HealthToken::balanceOf(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [staticcall]
│ └─ ← 2000000000000000000 [2e18]
├─ [0] VM::stopPrank()
│ └─ ← ()
└─ ← ()
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.15ms
Ran 1 test suite in 3.15ms: 1 tests passed, 0 failed, 0 skipped (1 total tests)
Tools Used
Foundry
Recommendations
Make these changes to the codebase:
mapping(address => uint256) private _collectedRewards;
mapping(uint256 => Listing) public tokenIdToListing;
+ mapping(address => mapping(uint256 => bool)) public IdTokenusedforrewards;
function collectReward() external {
require(!martenitsaToken.isProducer(msg.sender), "You are producer and not eligible for a reward!");
uint256 count = martenitsaToken.getCountMartenitsaTokensOwner(msg.sender);
- uint256 amountRewards = (count / requiredMartenitsaTokens) - _collectedRewards[msg.sender];
- if (amountRewards > 0) {
- _collectedRewards[msg.sender] = amountRewards;
- healthToken.distributeHealthToken(msg.sender, amountRewards);
- }
- }
+ uint256 nexttokenid = martenitsaToken.getNextTokenId();
+ for (uint256 i=0; i<nexttokenid; ++i){
+ if (msg.sender == martenitsaToken.ownerOf(i) && IdTokenusedforrewards[msg.sender][i] == true){
+ count -= 1;
+ }
+ else if (msg.sender == martenitsaToken.ownerOf(i) && IdTokenusedforrewards[msg.sender][i] == false){
+ IdTokenusedforrewards[msg.sender][i] = true;
+ }
+ }
+ uint256 amountRewards = (count / requiredMartenitsaTokens);
+ healthToken.distributeHealthToken(msg.sender, amountRewards);
+ }