Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Setup Slither #7

Merged
merged 24 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/slither.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Slither Analysis

on:
push:
branches: [ development ]
paths:
- '**.sol'
pull_request:
paths:
- '**.sol'

jobs:
analyze:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Run Slither
uses: crytic/slither-action@v0.3.1
id: slither
with:
node-version: 16
sarif: results.sarif
fail-on: none

- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ steps.slither.outputs.sarif }}
2 changes: 1 addition & 1 deletion script/Deploy001_NftReward.s.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {NftReward} from "../src/NftReward.sol";
Expand Down
108 changes: 55 additions & 53 deletions src/NftReward.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
pragma solidity ^0.8.18;
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved

import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
Expand Down Expand Up @@ -60,26 +60,28 @@

/**
* @notice Contract initializer (replaces constructor)
* @param _tokenName NFT token name
* @param _tokenSymbol NFT token symbol
* @param _initialOwner Initial owner name
* @param _minter Minter address
* @param tokenName NFT token name
* @param tokenSymbol NFT token symbol
* @param initialOwner Initial owner name
* @param minterAddress Minter address
*/
function initialize(
string memory _tokenName,
string memory _tokenSymbol,
address _initialOwner,
address _minter
string memory tokenName,
string memory tokenSymbol,
address initialOwner,
address minterAddress
)
public
initializer
{
__ERC721_init(_tokenName, _tokenSymbol);
__Ownable_init(_initialOwner);
require(minterAddress != address(0), "Minter cannot be zero address");
require(initialOwner != address(0), "Initial owner cannot be zero address");
__ERC721_init(tokenName, tokenSymbol);
__Ownable_init(initialOwner);
__EIP712_init("NftReward-Domain", "1");
__UUPSUpgradeable_init();
__Pausable_init();
minter = _minter;
minter = minterAddress;
}

//==================
Expand All @@ -88,22 +90,22 @@

/**
* @notice Returns mint request digest which should be signed by `minter`
* @param _mintRequest Mint request data
* @param mintRequest Mint request data
* @return Mint request digest which should be signed by `minter`
*/
function getMintRequestDigest(MintRequest calldata _mintRequest) public view returns (bytes32) {
function getMintRequestDigest(MintRequest calldata mintRequest) public view returns (bytes32) {
// for `string[]` array type we need to hash all the array values first
bytes32[] memory valuesHashed = new bytes32[](_mintRequest.values.length);
for (uint256 i = 0; i < _mintRequest.values.length; i++) {
valuesHashed[i] = keccak256(bytes(_mintRequest.values[i]));
bytes32[] memory valuesHashed = new bytes32[](mintRequest.values.length);
for (uint256 i = 0; i < mintRequest.values.length; i++) {
valuesHashed[i] = keccak256(bytes(mintRequest.values[i]));
}
// return final hash
return _hashTypedDataV4(keccak256(abi.encode(
keccak256("MintRequest(address beneficiary,uint256 deadline,bytes32[] keys,uint256 nonce,string[] values)"),
_mintRequest.beneficiary,
_mintRequest.deadline,
keccak256(abi.encodePacked(_mintRequest.keys)),
_mintRequest.nonce,
mintRequest.beneficiary,
mintRequest.deadline,
keccak256(abi.encodePacked(mintRequest.keys)),
mintRequest.nonce,
keccak256(abi.encodePacked(valuesHashed))
)));
}
Expand All @@ -118,59 +120,58 @@

/**
* @notice Returns signer of the mint request
* @param _mintRequest Mint request data
* @param _signature Minter signature
* @param mintRequest Mint request data
* @param signature Minter signature
* @return Signer of the mint request
*/
function recover(
MintRequest calldata _mintRequest,
bytes calldata _signature
MintRequest calldata mintRequest,
bytes calldata signature
)
public
view
returns (address)
{
bytes32 digest = getMintRequestDigest(_mintRequest);
address signer = ECDSA.recover(digest, _signature);
bytes32 digest = getMintRequestDigest(mintRequest);
address signer = ECDSA.recover(digest, signature);
return signer;
}

/**
* @notice Mints a reward NFT to beneficiary who provided a mint request with valid minter's signature
* @param _mintRequest Mint request data
* @param _signature Minter signature
* @param mintRequest Mint request data
* @param signature Minter signature
*/
function safeMint(
MintRequest calldata _mintRequest,
bytes calldata _signature
MintRequest calldata mintRequest,
bytes calldata signature
)
public
whenNotPaused
{
// validation
require(recover(_mintRequest, _signature) == minter, "Signed not by minter");
require(msg.sender == _mintRequest.beneficiary, "Not eligible");
require(block.timestamp < _mintRequest.deadline, "Signature expired");
require(!nonceRedeemed[_mintRequest.nonce], "Already minted");
require(_mintRequest.keys.length == _mintRequest.values.length, "Key/value length mismatch");
require(recover(mintRequest, signature) == minter, "Signed not by minter");
require(msg.sender == mintRequest.beneficiary, "Not eligible");
require(!nonceRedeemed[mintRequest.nonce], "Already minted");
require(mintRequest.keys.length == mintRequest.values.length, "Key/value length mismatch");

// mark nonce as used
nonceRedeemed[_mintRequest.nonce] = true;
nonceRedeemed[mintRequest.nonce] = true;

// save arbitrary token data
uint256 keysCount = _mintRequest.keys.length;
uint256 keysCount = mintRequest.keys.length;
for (uint256 i = 0; i < keysCount; i++) {
// save data
tokenData[tokenIdCounter][_mintRequest.keys[i]] = _mintRequest.values[i];
tokenData[tokenIdCounter][mintRequest.keys[i]] = mintRequest.values[i];
// save arbitrary token data key if not saved yet
if (!tokenDataKeyExists[_mintRequest.keys[i]]) {
tokenDataKeys.push(_mintRequest.keys[i]);
tokenDataKeyExists[_mintRequest.keys[i]] = true;
if (!tokenDataKeyExists[mintRequest.keys[i]]) {
tokenDataKeys.push(mintRequest.keys[i]);
tokenDataKeyExists[mintRequest.keys[i]] = true;
}
}

// mint token to beneficiary
_safeMint(_mintRequest.beneficiary, tokenIdCounter++);
_safeMint(mintRequest.beneficiary, tokenIdCounter++);
}

//=================
Expand All @@ -186,18 +187,19 @@

/**
* @notice Sets new base URI for all of the tokens
* @param _newBaseUri New base URI
* @param newBaseUri New base URI
*/
function setBaseUri(string memory _newBaseUri) external onlyOwner {
baseUri = _newBaseUri;
function setBaseUri(string memory newBaseUri) external onlyOwner {
baseUri = newBaseUri;
}

/**
* @notice Sets new minter address (who can sign off-chain mint requests)
* @param _newMinter New minter address
* @param newMinter New minter address
*/
function setMinter(address _newMinter) external onlyOwner {
minter = _newMinter;
function setMinter(address newMinter) external onlyOwner {
require(newMinter != address(0), "Minter cannot be zero address");
minter = newMinter;
}

/**
Expand All @@ -215,11 +217,11 @@

/**
* @notice Invalidates nonce value to prevent mint request reusage
* @param _nonceValue Nonce value to invalidate
* @param nonceValue Nonce value to invalidate
*/
function invalidateNonce(uint256 _nonceValue) external onlyOwner {
require(!nonceRedeemed[_nonceValue], "Already minted");
nonceRedeemed[_nonceValue] = true;
function invalidateNonce(uint256 nonceValue) external onlyOwner {
require(!nonceRedeemed[nonceValue], "Already minted");
nonceRedeemed[nonceValue] = true;
}

//====================
Expand Down
67 changes: 38 additions & 29 deletions test/NftReward.t.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
pragma solidity ^0.8.20;

import {Test, console} from "forge-std/Test.sol";
import {NftReward} from "../src/NftReward.sol";
Expand All @@ -16,7 +16,7 @@ contract NftRewardTest is Test {
address minter = vm.addr(minterPrivateKey);

bytes public data;

function setUp() public {
vm.prank(owner);

Expand All @@ -34,6 +34,36 @@ contract NftRewardTest is Test {
nftReward = NftReward(address(proxy));
}

function testSetup_ShouldRevert_IfMinterIsZeroAddress() public {
vm.prank(owner);
data = abi.encodeWithSignature(
"initialize(string,string,address,address)",
"NFT reward",
"RWD",
owner,
address(0)
);
nftReward = new NftReward();
vm.expectRevert("Minter cannot be zero address");
ERC1967Proxy proxy = new ERC1967Proxy(address(nftReward), data);
nftReward = NftReward(address(proxy));
}

function testSetup_ShouldRevert_IfOwnerIsZeroAddress() public {
vm.prank(owner);
data = abi.encodeWithSignature(
"initialize(string,string,address,address)",
"NFT reward",
"RWD",
address(0),
minter
);
nftReward = new NftReward();
vm.expectRevert("Initial owner cannot be zero address");
ERC1967Proxy proxy = new ERC1967Proxy(address(nftReward), data);
nftReward = NftReward(address(proxy));
}

//==================
// Public methods
//==================
Expand Down Expand Up @@ -215,33 +245,6 @@ contract NftRewardTest is Test {
nftReward.safeMint(mintRequest, signature);
}

function testSafeMint_ShouldRevert_IfSignatureExpired() public {
// prepare arbitrary data keys
bytes32[] memory keys = new bytes32[](1);
keys[0] = keccak256("GITHUB_ORGANIZATION_NAME");
// prepare arbitrary data values
string[] memory values = new string[](1);
values[0] = "ubiquity";
// prepare mint request
NftReward.MintRequest memory mintRequest = NftReward.MintRequest({
beneficiary: user1,
deadline: block.timestamp - 1, // set expired signature
keys: keys,
nonce: 1,
values: values
});
// get mint request digest which should be signed
bytes32 digest = nftReward.getMintRequestDigest(mintRequest);
// minter signs mint request digest
(uint8 v, bytes32 r, bytes32 s) = vm.sign(minterPrivateKey, digest);
// get minter's signature
bytes memory signature = abi.encodePacked(r, s, v);

vm.prank(user1);
vm.expectRevert('Signature expired');
nftReward.safeMint(mintRequest, signature);
}

function testSafeMint_ShouldRevert_IfNonceAlreadyUsed() public {
// prepare arbitrary data keys
bytes32[] memory keys = new bytes32[](1);
Expand Down Expand Up @@ -528,6 +531,12 @@ contract NftRewardTest is Test {
assertFalse(nftReward.paused());
}

function testSetMinter_ShouldRevert_IfMinterIsZeroAddress() public {
vm.prank(owner);
vm.expectRevert("Minter cannot be zero address");
nftReward.setMinter(address(0));
}

function testInvalidateNonce_ShouldInvalidateNonce() public {
// prepare arbitrary data keys
bytes32[] memory keys = new bytes32[](1);
Expand Down