Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ function test_claim_revert_invalidDistribution() public {
// ...
}
```

## Archiving Previous Contract Versions

To maintain a clear version history and ensure traceability of contract changes, we need to archive previous versions of smart contracts rather than deleting or overwriting them.
We use to archive a contract: [`forge flatten src/your-contract-name > archive/your-contract-name`](https://book.getfoundry.sh/forge/fmt/). “Flattening” a smart contracts basically copies the contract and all its dependencies into a single file.
Example: `forge flatten src/FANtiumAthletesV10.sol > src/archive/FANtiumAthletesV10.sol` Note: this should be done before you make any changes to the contract.
6 changes: 3 additions & 3 deletions script/DeployTestnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IERC20MetadataUpgradeable } from
"@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { Script } from "forge-std/Script.sol";
import { FANtiumAthletesV10 } from "src/FANtiumAthletesV10.sol";
import { FANtiumAthletesV11 } from "src/FANtiumAthletesV11.sol";
import { FANtiumClaimingV5 } from "src/FANtiumClaimingV5.sol";
import { FANtiumMarketplaceV1 } from "src/FANtiumMarketplaceV1.sol";
import { FANtiumTokenV1 } from "src/FANtiumTokenV1.sol";
Expand Down Expand Up @@ -35,9 +35,9 @@ contract DeployTestnet is Script {

function run() public {
vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY"));
FANtiumAthletesV10 fantiumAthletes = FANtiumAthletesV10(
FANtiumAthletesV11 fantiumAthletes = FANtiumAthletesV11(
UnsafeUpgrades.deployUUPSProxy(
address(new FANtiumAthletesV10()), abi.encodeCall(FANtiumAthletesV10.initialize, (ADMIN))
address(new FANtiumAthletesV11()), abi.encodeCall(FANtiumAthletesV11.initialize, (ADMIN))
)
);

Expand Down
4 changes: 2 additions & 2 deletions script/UpgradeMainnetV10.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Options } from "@openzeppelin/foundry-upgrades/Options.sol";
import { Core } from "@openzeppelin/foundry-upgrades/internal/Core.sol";
import { Script } from "forge-std/Script.sol";

contract UpgradeMainnetV10 is Script {
contract UpgradeMainnetV11 is Script {
error OnlyPolygonMainnet();

function run() public {
Expand All @@ -17,7 +17,7 @@ contract UpgradeMainnetV10 is Script {
vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY"));

Options memory opts;
Core.prepareUpgrade("FANtiumAthletesV10.sol:FANtiumAthletesV10", opts);
Core.prepareUpgrade("FANtiumAthletesV11.sol:FANtiumAthletesV11", opts);
Core.prepareUpgrade("FANtiumClaimingV5.sol:FANtiumClaimingV5", opts);
vm.stopBroadcast();
}
Expand Down
2 changes: 1 addition & 1 deletion script/UpgradeTestnet.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ contract UpgradeTestnet is Script {
vm.createSelectFork(vm.rpcUrl("amoy"));
vm.startBroadcast(vm.envUint("ADMIN_PRIVATE_KEY"));
if (FANTIUM_ATHLETES_UPGRADE) {
Upgrades.upgradeProxy(FANTIUM_ATHLETES_PROXY, "FANtiumAthletesV10.sol:FANtiumAthletesV10", "");
Upgrades.upgradeProxy(FANTIUM_ATHLETES_PROXY, "FANtiumAthletesV11.sol:FANtiumAthletesV11", "");
}

if (FANTIUM_CLAIMING_UPGRADE) {
Expand Down
83 changes: 53 additions & 30 deletions src/FANtiumAthletesV10.sol → src/FANtiumAthletesV11.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import { IFANtiumAthletes, MintRequest, VerificationStatus } from "./interfaces/IFANtiumAthletes.sol";
import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
Expand All @@ -15,6 +16,9 @@ import {
} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import { StringsUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol";
import { ECDSAUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol";
import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import { ECDSA } from "solady/utils/ECDSA.sol";
import { EIP712 } from "solady/utils/EIP712.sol";
import {
Collection,
CollectionData,
Expand All @@ -27,18 +31,19 @@ import { Rescue } from "src/utils/Rescue.sol";
import { TokenVersionUtil } from "src/utils/TokenVersionUtil.sol";

/**
* @title FANtium Athletes ERC721 contract V10.
* @title FANtium Athletes ERC721 contract V11.
* @author Mathieu Bour, Alex Chernetsky - FANtium AG, based on previous work by MTX studio AG.
* @custom:oz-upgrades-from src/archive/FANtiumAthletesV9.sol:FANtiumAthletesV9
* @custom:oz-upgrades-from src/archive/FANtiumAthletesV10.sol:FANtiumAthletesV10
*/
contract FANtiumAthletesV10 is
contract FANtiumAthletesV11 is
Initializable,
ERC721Upgradeable,
UUPSUpgradeable,
AccessControlUpgradeable,
PausableUpgradeable,
Rescue,
IFANtiumAthletes
IFANtiumAthletes,
EIP712
{
using StringsUpgradeable for uint256;
using ECDSAUpgradeable for bytes32;
Expand Down Expand Up @@ -71,6 +76,10 @@ contract FANtiumAthletesV10 is
*/
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

/// @notice EIP-712 typehash for KYC status struct
bytes32 public constant VERIFICATION_STATUS_TYPEHASH =
keccak256("VerificationStatus(address account,uint8 level,uint256 expiresAt)");

// ========================================================================
// State variables
// ========================================================================
Expand Down Expand Up @@ -153,11 +162,14 @@ contract FANtiumAthletesV10 is
__UUPSUpgradeable_init();
__AccessControl_init();
__Pausable_init();

_grantRole(DEFAULT_ADMIN_ROLE, admin);
nextCollectionId = 1;
}

function _domainNameAndVersion() internal pure override returns (string memory name, string memory version) {
return (NAME, "11");
}

/**
* @notice Implementation of the upgrade authorization logic
* @dev Restricted to the DEFAULT_ADMIN_ROLE
Expand Down Expand Up @@ -575,44 +587,55 @@ contract FANtiumAthletesV10 is
emit Sale(collectionId, quantity, recipient, amount, discount);
}

// ========================================================================
/**
* @notice Purchase NFTs from the sale.
* @param collectionId The collection ID to purchase from.
* @param quantity The quantity of NFTs to purchase.
* @param recipient The recipient of the NFTs.
* @dev Verifies the KYC status signature
* @param verificationStatus The KYC status to verify
* @param signature The backend-generated signature for user purchasing the athlete NFT
*/
function mintTo(uint256 collectionId, uint24 quantity, address recipient) public whenNotPaused returns (uint256) {
uint256 amount = _expectedPrice(collectionId, quantity);
return _mintTo(collectionId, quantity, amount, recipient);
function _verifySignature(VerificationStatus calldata verificationStatus, bytes calldata signature) internal view {
bytes32 kycStatusHash = keccak256(
abi.encode(
VERIFICATION_STATUS_TYPEHASH,
verificationStatus.account,
verificationStatus.level,
verificationStatus.expiresAt
)
);

bytes32 digest = _hashTypedData(kycStatusHash);
address signer = ECDSA.recover(digest, signature);

if (!hasRole(SIGNER_ROLE, signer)) {
revert InvalidMint(MintErrorReason.INVALID_SIGNATURE);
}
}

/**
* @notice Purchase NFTs from the sale with a custom price, checked
* @param collectionId The collection ID to purchase from.
* @param quantity The quantity of NFTs to purchase.
* @param recipient The recipient of the NFTs.
* @param amount The amount of tokens to purchase the NFTs with.
* @param signature The signature of the purchase request.
* @notice Purchase NFTs from the sale.
* @param mintRequest All the data required for purchase: collectionId, quantity, recipient etc.
* @param signature The backend-generated signature for user purchasing the athlete NFT
*/
function mintTo(
uint256 collectionId,
uint24 quantity,
address recipient,
uint256 amount,
bytes memory signature
MintRequest calldata mintRequest,
bytes calldata signature
)
public
external
whenNotPaused
returns (uint256)
{
bytes32 hash =
keccak256(abi.encode(collectionId, quantity, recipient, amount, nonces[recipient])).toEthSignedMessageHash();
if (!hasRole(SIGNER_ROLE, hash.recover(signature))) {
revert InvalidMint(MintErrorReason.INVALID_SIGNATURE);
_verifySignature(mintRequest.verificationStatus, signature);

// purchase requires AML check (level 1)
if (mintRequest.verificationStatus.level < 1) {
revert InvalidMint(MintErrorReason.ACCOUNT_NOT_KYCED);
}

nonces[recipient]++;
return _mintTo(collectionId, quantity, amount, recipient);
if (mintRequest.verificationStatus.expiresAt < block.timestamp) {
revert InvalidMint(MintErrorReason.SIGNATURE_EXPIRED);
}
// todo: we do NOT include amount + quantity into signature. Security vulnerability ?
return _mintTo(mintRequest.collectionId, mintRequest.quantity, mintRequest.amount, mintRequest.recipient);
}

// ========================================================================
Expand Down
Loading
Loading