xanoutas/solidity-security-findings huggingface dataset:
`accRewardsPerNFT` in `govNFT` can be front run
Audit: 2022-12-tigris-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/GovNFT.sol#L287-L294
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/GovNFT.sol#L50-L57


# Vulnerability details

## Impact

Currently the reward is only recorded when `distribute()` is called. This mechanism is not fair for the users and could even be abused to effectively steal part of the reward.

Impacts:
- Some users might just come after the `distribute()` and miss it, effectively lose part of the rewards.
- Malicious user could monitor the mempool for `distribute()`, frontrun it by minting large amount of govNFT to get the reward, and `claim()` right after. Effectively steal the reward.

## Proof of Concept

Imagine the following 2 scenarios:

1. bad luck Bob

- at the beginning there is only 1 govNFT.
- the trading contract calls `gov.distribute(tigUSD, 100e18)`, with 100 tigUSD reward.
- Bob mints 1 govNFT just after the `distribute()`
- Bob burns the govNFT after some time
- just after Bob burns, `gov.distribute(tigUSD, 100e18)` is called again with 100 tigUSD reward.

Bob misses all the reward he could have for 50 + 50 = 100 tigUSD.

2. sniper Alice

- at the beginning there is only 1 govNFT.
- the trading contract calls `gov.distribute(tigUSD, 100e18)`, with 100 tigUSD reward.
- Alice frontrun the txn and mint 1 govNFT, so Alice gets 50 tigUSD as reward. 
- Alice could choose to claim the reward right away through `Lock.sol#claimGovFees()`.

The issue is that `accRewardsPerNFT` will only be updated when `distribute()` is called, and the reward each user gets is based on the govNFT balance. 

```solidity
File: contracts/GovNFT.sol
287:     function distribute(address _tigAsset, uint _amount) external {

289:         try IERC20(_tigAsset).transferFrom(_msgSender(), address(this), _amount) {
290:             accRewardsPerNFT[_tigAsset] += _amount/totalSupply();

50:     function _mint(address to, uint256 tokenId) internal override {

54:             userPaid[to][assets[i]] += accRewardsPerNFT[assets[i]];
```


## Tools Used
Manual analysis.

## Recommended Mitigation Steps

Consider use some rewardRate-based gradual release model, such as Synthetix's StakingRewards contract.
`accRewardsPerShare` in `bondNFT` can be front run
Audit: 2022-12-tigris-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/BondNFT.sol#L211-L228
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/BondNFT.sol#L306-L314


# Vulnerability details

## Impact

Currently the reward `accRewardsPerShare` is only updated when `distribute()` is called. This mechanism is not fair for the users and could even be abused to effectively steal part of the reward.

Impacts:
- Some users might just come after the `distribute()` and miss it, effectively lose part of the rewards.
- Malicious user could monitor the mempool for `distribute()`, frontrun it by minting large amount of bond share to get the reward, and `claim()` right after. Effectively steal the reward.

## Proof of Concept

Imagine the following 2 scenarios:

1. bad luck Bob

- at the beginning there is only 100 total bond share.
- the Lock contract calls `bondNFT.distribute(tigUSD, 100e18)`, with 100 tigUSD reward.
- Bob locks and gets 100 shares just after the `distribute()`
- Bob burns the 100 shares after 7 days
- just after Bob burns, `bondNFT.distribute(tigUSD, 100e18)` is called again with 100 tigUSD reward.

Bob misses all the reward he could have for 50 + 50 = 100 tigUSD.

2. sniper Alice

- at the beginning there is only 100 total bond share.
- the Lock contract calls `bondNFT.distribute(tigUSD, 100e18)`, with 100 tigUSD reward.
- Alice frontrun the txn, locks and mints 100 shares, so Alice gets 50 tigUSD as reward. 
- Alice could choose to claim the reward right away through `Lock.sol#claim()`.

The issue is that `accRewardsPerShare` will only be updated when `distribute()` is called, and the reward each user gets is based on the bond share balance. 

```solidity
File: contracts/BondNFT.sol
211:     function distribute() external {

216:         IERC20(_tigAsset).transferFrom(_msgSender(), address(this), _amount);

225:             accRewardsPerShare[_tigAsset][aEpoch] += _amount * 1e18 / totalShares[_tigAsset];


306:     function _mint() internal {

311:             bondPaid[bond.id][bond.asset] = accRewardsPerShare[bond.asset][epoch[bond.asset]] * bond.shares / 1e18;
```


## Tools Used
Manual analysis.

## Recommended Mitigation Steps

Consider use some rewardRate-based gradual release model, such as Synthetix's StakingRewards contract.
Upgraded Q -> M from #50 [1674461707004]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #50 as M risk. The relevant finding follows:

 QA10. https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L44
The deposit() function only works for tokens that have no more than 18 decimals. This needs to be documented.
Upgraded Q -> M from #445 [1674423223201]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #445 as M risk. The relevant finding follows:

 [[5]]
Function crossChain() in GovNFT should have limit for maximum tokens allowed to be transferred, because of gas limit in the dest chain. if a user transferred a lot of tokens because there was two loop inside each other in handling logic(one loop through NFT token ids and one loop through assets list so the execution is in order #NFTs * #Assets) so transaction can be revert in the destination chain.
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/GovNFT.sol#L124-L165
Upgraded Q -> M from #658 [1674423108320]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #658 as M risk. The relevant finding follows:

 [L-05] MARGIN ASSET TOKENS WITH MORE THAN 18 DECIMALS ARE NOT SUPPORTED
As shown below, arithmetic operations of the StableVault.deposit, StableVault.withdraw, Trading._handleDeposit, and Trading._handleWithdraw functions that subtract the margin asset tokens' decimals will underflow if these decimals are more than 18; in this case, calling these functions will revert. This means that the protocol cannot scale to support margin asset tokens that have more than 18 decimals in the future. To prevent the described issue, please consider updating these functions' arithmetic operations to use divisions that divide 10**18 by 10**n, where n is the corresponding token decimals instead of subtracting such token decimals from 18.

https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/StableVault.sol#L44-L51

    function deposit(address _token, uint256 _amount) public {
        ...
        IERC20Mintable(stable).mintFor(
            _msgSender(),
            _amount*(10**(18-IERC20Mintable(_token).decimals()))
        );
    }
https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/StableVault.sol#L65-L72

    function withdraw(address _token, uint256 _amount) external returns (uint256 _output) {
        ...
        _output = _amount/10**(18-IERC20Mintable(_token).decimals());
        ...
    }
https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Trading.sol#L643-L659

    function _handleDeposit(address _tigAsset, address _marginAsset, uint256 _margin, address _stableVault, ERC20PermitData calldata _permitData, address _trader) internal {
        ...
        if (_tigAsset != _marginAsset) {
            ...
            uint _marginDecMultiplier = 10**(18-ExtendedIERC20(_marginAsset).decimals());
            ...
        } else {
            ...
        }        
    }
https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Trading.sol#L668-L678

    function _handleWithdraw(IPosition.Trade memory _trade, address _stableVault, address _outputToken, uint _toMint) internal {
        ...
        if (_outputToken == _trade.tigAsset) {
            ...
        } else {
            ...
            if (IERC20(_outputToken).balanceOf(address(this)) != _balBefore + _toMint/(10**(18-ExtendedIERC20(_outputToken).decimals()))) revert BadWithdraw();
            ...
        }        
    }
Upgraded Q -> M from #658 [1674423084300]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #658 as M risk. The relevant finding follows:

 [L-04] USDT IS NOT SUPPORTED FOR CALLING Trading._handleDeposit FUNCTION ON ETHEREUM MAINNET
As shown by https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L205, USDT on the Ethereum mainnet does not allow approving a new amount when the existing approved amount is not zero. On the Ethereum mainnet, when calling the following Trading._handleDeposit function with USDT being the margin asset, executing IStableVault(_stableVault).deposit(_marginAsset, _margin/_marginDecMultiplier) will not use all of the approved type(uint).max amount so executing IERC20(_marginAsset).approve(_stableVault, type(uint).max) can revert when calling this function for the second time. In the future, if the protocol wants to support USDT as a margin asset on the Ethereum mainnet, IERC20(_marginAsset).approve(_stableVault, 0); can be added before the line of IERC20(_marginAsset).approve(_stableVault, type(uint).max); in the Trading._handleDeposit function.

https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Trading.sol#L643-L659

    function _handleDeposit(address _tigAsset, address _marginAsset, uint256 _margin, address _stableVault, ERC20PermitData calldata _permitData, address _trader) internal {
        IStable tigAsset = IStable(_tigAsset);
        if (_tigAsset != _marginAsset) {
            ...
            IERC20(_marginAsset).transferFrom(_trader, address(this), _margin/_marginDecMultiplier);
            IERC20(_marginAsset).approve(_stableVault, type(uint).max);
            IStableVault(_stableVault).deposit(_marginAsset, _margin/_marginDecMultiplier);
            ...
        } else {
            ...
        }        
    }
Upgraded Q -> M from #97 [1674422838137]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #97 as M risk. The relevant finding follows:

 DOS StableVault deposit and withdraws if ERC20 with more than 18 decimals used.
Description
withdraw and deposit functions in StableVault will revert if ERC20 token with more than 18 decimals is used.
This is due to an revert when trying to subtract the token decimals from hardcoded 18.

deposit:
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L49

    function deposit(address _token, uint256 _amount) public {
        require(allowed[_token], "Token not listed");
        IERC20(_token).transferFrom(_msgSender(), address(this), _amount);
        IERC20Mintable(stable).mintFor(
            _msgSender(),
            _amount*(10**(18-IERC20Mintable(_token).decimals()))
        );
    }
Recommendation:
Add a check that token decimals is under 18 in listToken.
withdraw:
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L67:

    function withdraw(address _token, uint256 _amount) external returns (uint256 _output) {
        IERC20Mintable(stable).burnFrom(_msgSender(), _amount);
        _output = _amount/10**(18-IERC20Mintable(_token).decimals());
        IERC20(_token).transfer(
            _msgSender(),
            _output
        );
    }
-----
Upgraded Q -> M from #113 [1674422768939]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #113 as M risk. The relevant finding follows:

 During handling the open fees, the _tigAsset is distributed to gov. But, it is not approved before to be consumed by gov. So, the first user's transaction to initiate a market order, will fail.
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/Trading.sol#L749

During handling the close fees, the approve max is applied every time before distributing to gov. Actually, this is redundant to approve max every time.
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/Trading.sol#L807

So, the following check is better to be added before distributing to gov in both functions _handleOpenFees and _handleCloseFees:

if(IStable(_tigAsset).allowance(address(this), address(gov)) < _daoFeesPaid){
            IStable(_tigAsset).approve(address(gov), type(uint).max);
        }
Upgraded Q -> M from #164 [1674419095024]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #164 as M risk. The relevant finding follows:

 [LOW‑1] The Contract Should approve(0) First
Some tokens (like USDT L199) do not work when changing the allowance from an existing non-zero allowance value. They must first be approved by zero and then the actual allowance must be approved.

Proof Of Concept
117: IERC20(assets[i]).approve(address(bondNFT), type(uint256).max);
https://github.com/code-423n4/2022-12-tigris/tree/main/contracts/Lock.sol#L117

652: IERC20(_marginAsset).approve(_stableVault, type(uint).max);
https://github.com/code-423n4/2022-12-tigris/tree/main/contracts/Trading.sol#L652

Recommended Mitigation Steps
Approve with a zero amount first before setting the actual amount.
Upgraded Q -> M from #254 [1674418824740]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #254 as M risk. The relevant finding follows:

 03: Lock.claimGovFees() will revert with tokens that have approve race condition protection
Some tokens only allow you to approve a new value if the current allowance is set to zero, e.g. USDT.

This is not an issue right now on Polygon or Arbitrum. But, stablecoin contracts on those networks are upgradable and migth change. Also, if you decide to deploy the protocol to other networks, you might encounter this problem.
Upgraded Q -> M from #268 [1674418407759]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #268 as M risk. The relevant finding follows:

 [L-02] Front running attacks by the owner
Project has one possible attack vectors by the onlyOwner:

dao.Fees , burnsFees , referralFees , botFees variable;
It determines the fees rate
The default deposit fees equal zero.
Can be updated by onlyOwner with function Fees

When a user use feed, expecting to have zero fee , the owner can frontrun the fee function and increase fees to 10000 bps , If the size is big enough, that may be a significant amount of money.

contracts/Trading.sol:
  951       */
  952:     function setFees(bool _open, uint _daoFees, uint _burnFees, uint _referralFees, uint _botFees, uint _percent) external onlyOwner {
  953:         unchecked {
  954:             require(_daoFees >= _botFees+_referralFees*2);
  955:             if (_open) {
  956:                 openFees.daoFees = _daoFees;
  957:                 openFees.burnFees = _burnFees;
  958:                 openFees.referralFees = _referralFees;
  959:                 openFees.botFees = _botFees;
  960:             } else {
  961:                 closeFees.daoFees = _daoFees;
  962:                 closeFees.burnFees = _burnFees;
  963:                 closeFees.referralFees = _referralFees;
  964:                 closeFees.botFees = _botFees;                
  965:             }
  966:             require(_percent <= DIVISION_CONSTANT);
  967:             vaultFundingPercent = _percent;
  968:         }
  969:     }
Recommended Mitigation Steps
Use a timelock to avoid instant changes of the parameters.
Upgraded Q -> M from #351 [1674418158719]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #351 as M risk. The relevant finding follows:

 Stable Vault cannot accept tokens with more then 18 decimals because of this lines:
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L49
https://github.com/code-423n4/2022-12-tigris/blob/588c84b7bb354d20cbca6034544c4faa46e6a80e/contracts/StableVault.sol#L67
Consider using different conversion formula. For example: amount * 1e18 / 10**token.decimals() for deposit and amount * 10**token.decimals() / 1e18 for withdraw
Upgraded Q -> M from #32 [1671721748112]
Audit: 2022-12-tigris-findings
Severity: MEDIUM

Judge has assessed an item in Issue #32 as M risk. The relevant finding follows:

 2. StableVault deposits are limited to 18 decimals
During deposit and withdraw to/from StableVault contract, it mints/burns the same amount of stable tokens with respect to decimals. The current implementation supports only tokens with <= 18 decimals. It’s not possible to list a token with a bigger amount of decimals because it would cause overflow exceptions during stable token decimals calculation: 18-IERC20Mintable(_token).decimals().
Path: ./contracts/StableVault.sol : deposit()
INITREFS() COULD BE USED TO OVERWRITE ALL REFFERAL DATA
Audit: 2022-12-tigris-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Referrals.sol#L60-L76


# Vulnerability details

## Impact
Although denoted as deprecated, `initRefs()` is readily there in `Referrals.sol` that could be called by the owner whose private keys might have been compromised to reassign all the referral data. This could impact the referrers specifically who might have already put in a great effort referring a big pool of traders to Tigris Trade.        

## Proof of Concept
[Referrals.sol#L60-L76](https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/Referrals.sol#L60-L76)

```
    function initRefs(
        address[] memory _codeOwners,
        bytes32[] memory _ownedCodes,
        address[] memory _referredA,
        bytes32[] memory _referredTo
    ) external onlyOwner {
        require(!isInit);
        isInit = true;
        uint _codeOwnersL = _codeOwners.length;
        uint _referredAL = _referredA.length;
        for (uint i=0; i<_codeOwnersL; i++) {
            _referral[_ownedCodes[i]] = _codeOwners[i];
        }
        for (uint i=0; i<_referredAL; i++) {
            _referred[_referredA[i]] = _referredTo[i];
        }
    }
```
Note that the compromised or malicious owner might wait for a time when `bytes32[] memory _ownedCodes` and `address[] memory _referredA` have grown to big lists since `initRefs()` could only be called once because of `isInit`. To fully exploit the call, the owner would limit `address[] memory _codeOwners` and `bytes32[] memory _referredTo` all to himself or to addresses and codes under his control.

## Recommended Mitigation Steps
It is recommended removing `initRefs()` and `isInit` from the contract to make the protocol more in full trust by the users.
Chainlink price feed is not sufficiently validated and can return stale price
Audit: 2022-12-tigris-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/utils/TradingLibrary.sol#L91-L122


# Vulnerability details

## Impact
As mentioned by https://docs.tigris.trade/protocol/oracle, "Prices provided by the oracle network are also compared to Chainlink's public price feeds for additional security. If prices have more than a 2% difference the transaction is reverted." The Chainlink price verification logic in the following `TradingLibrary.verifyPrice` function serves this purpose. However, besides that `IPrice(_chainlinkFeed).latestAnswer()` uses Chainlink's deprecated `latestAnswer` function, this function also does not guarantee that the price returned by the Chainlink price feed is not stale. When `assetChainlinkPriceInt != 0` is `true`, it is still possible that `assetChainlinkPriceInt` is stale in which the Chainlink price verification would compare the off-chain price against a stale price returned by the Chainlink price feed. For a off-chain price that has more than a 2% difference when comparing to a more current price returned by the Chainlink price feed, this off-chain price can be incorrectly considered to have less than a 2% difference when comparing to a stale price returned by the Chainlink price feed. As a result, a trading transaction that should revert can go through, which makes the price verification much less secure.

https://github.com/code-423n4/2022-12-tigris/blob/main/contracts/utils/TradingLibrary.sol#L91-L122
```solidity
    function verifyPrice(
        uint256 _validSignatureTimer,
        uint256 _asset,
        bool _chainlinkEnabled,
        address _chainlinkFeed,
        PriceData calldata _priceData,
        bytes calldata _signature,
        mapping(address => bool) storage _isNode
    )
        external view
    {
        ...
        if (_chainlinkEnabled && _chainlinkFeed != address(0)) {
            int256 assetChainlinkPriceInt = IPrice(_chainlinkFeed).latestAnswer();
            if (assetChainlinkPriceInt != 0) {
                uint256 assetChainlinkPrice = uint256(assetChainlinkPriceInt) * 10**(18 - IPrice(_chainlinkFeed).decimals());
                require(
                    _priceData.price < assetChainlinkPrice+assetChainlinkPrice*2/100 &&
                    _priceData.price > assetChainlinkPrice-assetChainlinkPrice*2/100, "!chainlinkPrice"
                );
            }
        }
    }
```

Based on https://docs.chain.link/docs/historical-price-data, the followings can be done to avoid using a stale price returned by the Chainlink price feed.
1. The `latestRoundData` function can be used instead of the deprecated `latestAnswer` function.
2. `roundId` and `answeredInRound` are also returned. "You can check `answeredInRound` against the current `roundId`. If `answeredInRound` is less than `roundId`, the answer is being carried over. If `answeredInRound` is equal to `roundId`, then the answer is fresh."
3. "A read can revert if the caller is requesting the details of a r
Lack of validation on price feeds
Audit: 2022-12-tigris-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2022-12-tigris/blob/0cb05a462e78c4470662e9d9a4f9ab587f266bb5/contracts/utils/TradingLibrary.sol#L113-L114


# Vulnerability details

## Impact
Lack of validation on Chainlink price feeds may result in incorrectly functioning or non-functioning protocol.

For example:
- during high volatility a price feed may be suspended or become stale;
- on L2 networks the sequencer might be down
- on L2 networks the sequencer might be just restarted and a grace period should be waited for results to become reliable.

The results of using a deprecated API (as reported in my other issue opened related to Chainlink API) does not give a predictable response in these cases (may be 0, may be stale, may revert?).

## Proof of Concept

https://github.com/code-423n4/2022-12-tigris/blob/0cb05a462e78c4470662e9d9a4f9ab587f266bb5/contracts/utils/TradingLibrary.sol#L113-L114

```solidity
            int256 assetChainlinkPriceInt = IPrice(_chainlinkFeed).latestAnswer();
            if (assetChainlinkPriceInt != 0) {
                ...
            }
```

## Tools Used
Manual review

## Recommended Mitigation Steps

- Use [latestRoundData()](https://docs.chain.link/data-feeds/price-feeds/api-reference/#latestrounddata) API
- Use response's updatedAt (timestamp of when the round was updated) to verify the feed is not stale
- Refer to Chainlink documentation regarding sequencer handling with Arbitrum/Optimism L2 network