---
# System prepended metadata

title: Test contract for an interview with Solidity auditor candidates

---

# 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];
    }
}
```