# Test contract for an interview with Solidity auditor candidates
## Description
There is `Marketplace` contract which allows to buy and sell NFTs exchanging them for `PAYMENT_TOKEN`s. After the sale the funds remain on the balance of contract until claimed by seller. When claiming seller additionally gets a random reward in `REWARD_TOKEN` which amount depends on sale price and number of days passed from sale.
## Task
Audit contracts for possible vulnerabilities, bugs, problematic and nonoptimal behavior and provide a report in a Markdown file with a list of findings. Each finding should have:
- description of a problem
- line number in code
- recommendations for fixing a problem
## Contract
```solidity=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
interface IRewardToken is IERC20 {
function rewardUser(address owner, uint256 amount) external;
}
contract Rewardable {
using SafeMath for uint256;
error NothingForClaim();
struct Reward {
uint256 timestamp;
uint256 amount;
}
uint256 constant public PCT_DENOMINATOR = 1000;
uint256 private constant SEED = 335813536577843457;
IERC20 internal PAYMENT_TOKEN;
IRewardToken internal REWARD_TOKEN;
uint256 public _rewardsAmount;
// user => reward
mapping(address => Reward[]) internal _rewards;
constructor(address rewardToken, address paymentToken) {
REWARD_TOKEN = IRewardToken(rewardToken);
PAYMENT_TOKEN = IERC20(paymentToken);
}
function claim(address user) external {
uint256 length = _rewards[user].length;
if (length == 0) revert NothingForClaim();
for (uint256 i = 0; i < length; i++) {
Reward storage reward = _rewards[user][length - i];
withdrawLastDeposit(user, reward.amount);
payRewards(user, reward);
}
delete _rewards[user];
}
function payRewards(address user, Reward memory reward) internal {
uint256 random = uint256(keccak256(abi.encodePacked(block.timestamp, SEED)));
uint256 daysDelta = (block.timestamp - reward.timestamp) / 1 days;
uint256 userReward = reward.amount / PCT_DENOMINATOR * (random % daysDelta);
if (userReward > 0) {
REWARD_TOKEN.rewardUser(user, userReward);
}
}
function withdrawLastDeposit(address user, uint256 amount) internal {
_rewards[user].pop();
_rewardsAmount -= amount;
PAYMENT_TOKEN.transfer(user, amount);
}
function depositForRewards(address user, address payer, uint256 amount) internal {
PAYMENT_TOKEN.transferFrom(payer, address(this), amount);
_rewardsAmount += amount;
_rewards[user].push(Reward(block.timestamp, amount));
}
}
contract Marketplace is Rewardable {
error AlreadyOwner();
error NotItemOwner();
error InvalidSale();
error AlreadyOnSale();
struct ItemSale {
address seller;
uint256 price;
uint256 startTime;
}
IERC721 internal NFT_TOKEN;
// nft tokenId => item
mapping(uint256 => ItemSale) public items;
constructor(
address nftToken,
address paymentToken,
address rewardToken
) Rewardable(rewardToken, paymentToken) {
NFT_TOKEN = IERC721(nftToken);
}
function setForSale(uint256 tokenId, uint256 price, uint256 startTime) external {
if (NFT_TOKEN.ownerOf(tokenId) != msg.sender) revert NotItemOwner();
if (block.timestamp > startTime) revert InvalidSale();
if (items[tokenId].price == price) revert InvalidSale();
items[tokenId] = ItemSale(msg.sender, price, startTime);
}
function discardFromSale(uint256 tokenId) external {
if (NFT_TOKEN.ownerOf(tokenId) != msg.sender) revert NotItemOwner();
delete items[tokenId];
}
function postponeSale(uint256 tokenId, uint256 postponeSeconds) external {
if (NFT_TOKEN.ownerOf(tokenId) != msg.sender) revert NotItemOwner();
ItemSale storage item = items[tokenId];
assembly {
let s := add(item.slot, 2)
sstore(s, add(sload(s), postponeSeconds))
}
}
function buy(uint256 tokenId) external {
address owner = NFT_TOKEN.ownerOf(tokenId);
if (owner == msg.sender) revert AlreadyOwner();
if (block.timestamp < items[tokenId].startTime) revert InvalidSale();
if (items[tokenId].price == 0 ||
items[tokenId].seller == address(0) ||
items[tokenId].seller == msg.sender) revert InvalidSale();
depositForRewards(owner, msg.sender, items[tokenId].price);
NFT_TOKEN.transferFrom(owner, msg.sender, tokenId);
delete items[tokenId];
}
}
```