增加一个使用测试币购买中心化物品的合约

This commit is contained in:
CounterFire2023 2024-07-24 16:01:48 +08:00
parent 09789251e3
commit a716a86f23
14 changed files with 1669 additions and 2331 deletions

File diff suppressed because one or more lines are too long

View File

@ -2,8 +2,9 @@ const market = {
feeToAddress: "0x50A8e60041A206AcaA5F844a1104896224be6F39",
mallFeeAddress: "0x50A8e60041A206AcaA5F844a1104896224be6F39",
paymentTokens: [
"0x514609B71340E149Cb81A80A953D07A7Fe41bd4F", // USDT
"0x3b2d8a1931736fc321c24864bceee981b11c3c57", // USDC
],
verifier: "0x50A8e60041A206AcaA5F844a1104896224be6F39"
};
const admins = {

View File

@ -0,0 +1,94 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {HasSignature} from "../core/HasSignature.sol";
import {TimeChecker} from "../utils/TimeChecker.sol";
import {MallBase} from "./MallBase.sol";
/**
* @title GameItemMall
* @dev GameItemMall is a contract for managing centralized game items sale,
* allowing users to buy item in game.
*/
contract GameItemMall is MallBase, ReentrancyGuard, HasSignature, TimeChecker {
using SafeERC20 for IERC20;
mapping(uint256 itemId => address user) public orderIdUsed;
event ItemSoldOut(
address indexed buyer,
address indexed passport,
uint256 indexed orderId,
address currency,
uint256 amount
);
constructor(address _currency, address _feeToAddress, address _verifier, uint256 _duration)
TimeChecker(_duration)
MallBase(_currency, _feeToAddress, _verifier){
}
function buy(
address passport,
uint256 orderId,
address currency,
uint256 amount,
uint256 signTime,
uint256 saltNonce,
bytes calldata signature
) external nonReentrant signatureValid(signature) timeValid(signTime) {
require(passport != address(0), "passport address can not be zero");
// check if orderId is used
require(orderIdUsed[orderId] == address(0), "orderId is used");
// check if currency is supported
require(erc20Supported[currency], "currency is not supported");
// check if amount is valid
require(amount > 0, "amount is zero");
address buyer = _msgSender();
bytes32 criteriaMessageHash = getMessageHash(
buyer,
passport,
orderId,
currency,
amount,
_CACHED_THIS,
_CACHED_CHAIN_ID,
signTime,
saltNonce
);
checkSigner(verifier, criteriaMessageHash, signature);
IERC20 paymentContract = IERC20(currency);
_useSignature(signature);
orderIdUsed[orderId] = buyer;
paymentContract.safeTransferFrom(buyer, feeToAddress, amount);
emit ItemSoldOut(buyer, passport, orderId, currency, amount);
}
function getMessageHash(
address _buyer,
address _passport,
uint256 _orderId,
address _currency,
uint256 _amount,
address _contract,
uint256 _chainId,
uint256 _signTime,
uint256 _saltNonce
) public pure returns (bytes32) {
bytes memory encoded = abi.encodePacked(
_buyer,
_passport,
_orderId,
_currency,
_amount,
_contract,
_chainId,
_signTime,
_saltNonce
);
return keccak256(encoded);
}
}

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
abstract contract MallBase is Ownable {
address public verifier;
// Address to receive transaction fee
address public feeToAddress;
uint256 public immutable _CACHED_CHAIN_ID;
address public immutable _CACHED_THIS;
mapping(address token => bool status) public erc20Supported;
event AddERC20Suppout(address erc20);
event RemoveERC20Suppout(address erc20);
event VerifierUpdated(address indexed verifier);
event FeeToAddressUpdated(address indexed feeToAddress);
constructor(address _currency, address _feeToAddress, address _verifier) {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_THIS = address(this);
verifier = _verifier;
erc20Supported[_currency] = true;
feeToAddress = _feeToAddress;
}
function addERC20Support(address erc20) external onlyOwner {
require(erc20 != address(0), "ERC20 address can not be zero");
erc20Supported[erc20] = true;
emit AddERC20Suppout(erc20);
}
function removeERC20Support(address erc20) external onlyOwner {
erc20Supported[erc20] = false;
emit RemoveERC20Suppout(erc20);
}
/**
* @dev update verifier address
*/
function updateVerifier(address _verifier) external onlyOwner {
require(_verifier != address(0), "address can not be zero");
verifier = _verifier;
emit VerifierUpdated(_verifier);
}
function setFeeToAddress(address _feeToAddress) external onlyOwner {
require(
_feeToAddress != address(0),
"fee received address can not be zero"
);
feeToAddress = _feeToAddress;
emit FeeToAddressUpdated(_feeToAddress);
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) Immutable Pty Ltd 2018 - 2024
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {ERC20Permit, ERC20} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {MintingAccessControl, AccessControl, IAccessControl} from "@imtbl/contracts/contracts/access/MintingAccessControl.sol";
import {IImmutableERC20Errors} from "@imtbl/contracts/contracts/token/erc20/preset/Errors.sol";
/**
* @notice ERC 20 contract that wraps Open Zeppelin's ERC 20 contract.
* This contract has the concept of a hubOwner, called _hubOwner in the constructor.
* This account has no rights to execute any administrative actions within the contract,
* with the exception of renouncing their ownership.
* The Immutable Hub uses this function to help associate the ERC 20 contract
* with a specific Immutable Hub account.
*/
contract ImmutableERC20MinterBurnerPermit is ERC20Capped, ERC20Burnable, ERC20Permit, MintingAccessControl {
/// @notice Role to mint tokens
bytes32 public constant HUB_OWNER_ROLE = bytes32("HUB_OWNER_ROLE");
/**
* @dev Delegate to Open Zeppelin's contract.
* @param _roleAdmin The account that has the DEFAULT_ADMIN_ROLE.
* @param _minterAdmin The account that has the MINTER_ROLE.
* @param _hubOwner The account that owns the contract and is associated with Immutable Hub.
* @param _name Name of the token.
* @param _symbol Token symbol.
* @param _maxTokenSupply The maximum supply of the token.
*/
constructor(
address _roleAdmin,
address _minterAdmin,
address _hubOwner,
string memory _name,
string memory _symbol,
uint256 _maxTokenSupply
) ERC20(_name, _symbol) ERC20Permit(_name) ERC20Capped(_maxTokenSupply) {
_grantRole(DEFAULT_ADMIN_ROLE, _roleAdmin);
_grantRole(HUB_OWNER_ROLE, _hubOwner);
_grantRole(MINTER_ROLE, _minterAdmin);
}
/**
* @dev Mints `amount` number of token and transfers them to the `to` address.
* @param to the address to mint the tokens to.
* @param amount The amount of tokens to mint.
*/
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
/**
* @dev Renounces the role `role` from the calling account. Prevents the last hub owner and admin from
* renouncing their role.
* @param role The role to renounce.
* @param account The account to renounce the role from.
*/
function renounceRole(bytes32 role, address account) public override(AccessControl, IAccessControl) {
if (getRoleMemberCount(role) == 1 && (role == HUB_OWNER_ROLE || role == DEFAULT_ADMIN_ROLE)) {
revert IImmutableERC20Errors.RenounceOwnershipNotAllowed();
}
super.renounceRole(role, account);
}
/**
* @dev Delegate to Open Zeppelin's ERC20Capped contract.
*/
function _mint(address account, uint256 amount) internal override(ERC20, ERC20Capped) {
ERC20Capped._mint(account, amount);
}
}

View File

@ -16,7 +16,7 @@ const deployNFTClaim: DeployFunction =
});
console.log("==NFTLock addr=", ret.address);
updateArray({
name: "NFTLock",
name: "NFTLockV2",
type: "logic",
json: "assets/contracts/NFTLock.json",
address: ret.address,

View File

@ -0,0 +1,29 @@
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
import { updateArray } from "../scripts/utils"
const deployNFTClaim: DeployFunction =
async function (hre: HardhatRuntimeEnvironment) {
const provider = hre.ethers.provider;
const from = await (await provider.getSigner()).getAddress();
const config = require(`../config/config_${hre.network.name}`);
const { mallFeeAddress, paymentTokens, verifier } = config.market
const ret = await hre.deployments.deploy("GameItemMall", {
from,
args: [paymentTokens[0], mallFeeAddress, verifier, 3600],
log: true,
});
console.log("==GameItemMall addr=", ret.address);
updateArray({
name: "GameItemMall",
type: "logic",
json: "assets/contracts/GameItemMall.json",
address: ret.address,
network: hre.network.name,
});
};
deployNFTClaim.tags = ["GameItemMall"];
export default deployNFTClaim;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -46,5 +46,17 @@
"type": "erc721",
"json": "assets/contracts/CFNFTGame.json",
"address": "0x994dE61dD536B22F7e3BDB77aa3ef55AeC938bFD"
},
{
"name": "NFTLockV2",
"type": "logic",
"json": "assets/contracts/NFTLock.json",
"address": "0xFb9B3FA9343020b98ba673c09f4b4539ef67Ee16"
},
{
"name": "GameItemMall",
"type": "logic",
"json": "assets/contracts/GameItemMall.json",
"address": "0xAbE8CCCd52840cd4a77B0C46DaD46bC628f8018D"
}
]

View File

@ -16,6 +16,7 @@
"deploy:nftlock:main": "hardhat deploy --tags NFTLockMain --network sepolia_test --reset",
"deploy:testtoken": "hardhat deploy --tags TestToken --network imtbl_test --reset",
"deploy:airdrop": "hardhat deploy --tags AirdropToken --network imtbl_test --reset",
"deploy:gameitemmall": "hardhat deploy --tags GameItemMall --network imtbl_test --reset",
"solhint": "solhint --config ./.solhint.json"
},
"author": "",

146
test/testGameItemMall.ts Normal file
View File

@ -0,0 +1,146 @@
import { expect } from 'chai'
import hre from "hardhat";
import {
getBytes,
solidityPackedKeccak256,
} from 'ethers'
import {
loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
describe('GameItemMall', function() {
async function deployOneContract() {
const [owner, otherAccount] = await hre.ethers.getSigners();
const verifier = owner.address;
const OperatorAllowlist = await hre.ethers.getContractFactory("OperatorAllowlist");
const operatorAllowlist = await OperatorAllowlist.deploy(owner.address);
const CFFT = await hre.ethers.getContractFactory("ImmutableERC20MinterBurnerPermit");
const ft = await CFFT.deploy(owner.address, owner.address, owner.address, "test usdc", "usdc", '100000000000000000000000000');
await ft.grantMinterRole(owner.address);
await ft.mint(otherAccount.address, '1000');
const GameItemMall = await hre.ethers.getContractFactory("GameItemMall");
const mall = await GameItemMall.deploy( ft.target, verifier, verifier, 3600);
const chainId = hre.network.config.chainId
await operatorAllowlist.grantRegistrarRole(owner.address)
await operatorAllowlist.addAddressToAllowlist([mall.target])
return { mall, owner, otherAccount, verifier, ft, chainId };
}
describe("Deployment", function () {
it('should deploy GameIteMall with the correct verifier', async function() {
const { mall, verifier } = await loadFixture(deployOneContract);
expect(await mall.verifier()).to.equal(verifier);
});
it('should deploy GameItemMall with the correct FT address', async function() {
const { mall, ft } = await loadFixture(deployOneContract);
expect(await mall.erc20Supported(ft.target)).to.equal(true);
});
})
describe("buy", function () {
it('should buy item success', async function() {
const { mall, ft, otherAccount, owner, chainId } = await loadFixture(deployOneContract);
const amount = 100;
// @ts-ignore
await ft.connect(otherAccount).approve(mall.target, amount);
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
const orderId = '100000001'
/**
function getMessageHash(
address _buyer,
address _passport,
uint256 _orderId,
address _currency,
uint256 _amount,
address _contract,
uint256 _chainId,
uint256 _signTime,
uint256 _saltNonce
)
*/
let localMsgHash = solidityPackedKeccak256(["address","address", "uint256", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, orderId, ft.target, amount, mall.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
/**
* function buy(
address passport,
uint256 orderId,
address currency,
uint256 amount,
uint256 signTime,
uint256 saltNonce,
bytes calldata signature
)
*/
//@ts-ignore
await mall.connect(otherAccount).buy(otherAccount.address, orderId, ft.target, amount, now, nonce, signature);
expect(await ft.balanceOf(otherAccount.address)).to.equal(900);
});
it('should revert buy item for signature error', async function() {
const { mall, ft, otherAccount, owner, chainId } = await loadFixture(deployOneContract);
const amount = 100;
// @ts-ignore
await ft.connect(otherAccount).approve(mall.target, amount);
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
const orderId = '100000001'
let localMsgHash = solidityPackedKeccak256(["address","address", "uint256", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, orderId, ft.target, amount, mall.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await expect(mall.connect(otherAccount).buy(otherAccount.address, orderId, ft.target, amount + 1, now, nonce, signature)).to.be.revertedWith("invalid signature");
});
it('should revert buy item for not approval', async function() {
const { mall, ft, otherAccount, owner, chainId } = await loadFixture(deployOneContract);
const amount = 100;
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
const orderId = '100000001'
let localMsgHash = solidityPackedKeccak256(["address","address", "uint256", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, orderId, ft.target, amount, mall.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await expect(mall.connect(otherAccount).buy(otherAccount.address, orderId, ft.target, amount, now, nonce, signature)).to.be.revertedWith("ERC20: insufficient allowance");
});
it('should revert buy item for signature used', async function() {
const { mall, ft, otherAccount, owner, chainId } = await loadFixture(deployOneContract);
const amount = 100;
// @ts-ignore
await ft.connect(otherAccount).approve(mall.target, amount);
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
const orderId = '100000001'
let localMsgHash = solidityPackedKeccak256(["address","address", "uint256", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, orderId, ft.target, amount, mall.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await mall.connect(otherAccount).buy(otherAccount.address, orderId, ft.target, amount, now, nonce, signature)
//@ts-ignore
await expect(mall.connect(otherAccount).buy(otherAccount.address, orderId, ft.target, amount, now, nonce, signature)).to.be.revertedWith("signature used. please send another transaction with new signature");
});
it('should revert buy item for unsupport currency', async function() {
const { mall, ft, otherAccount, owner, chainId } = await loadFixture(deployOneContract);
const amount = 100;
// @ts-ignore
await ft.connect(otherAccount).approve(mall.target, amount);
const nonce = (Math.random() * 1000) | 0;
const now = Date.now() / 1000 | 0;
const orderId = '100000001'
await mall.removeERC20Support(ft.target);
let localMsgHash = solidityPackedKeccak256(["address","address", "uint256", "address", "uint256", "address", "uint256", "uint256", "uint256"],
[otherAccount.address, otherAccount.address, orderId, ft.target, amount, mall.target, chainId, now, nonce]);
const signature = await owner.signMessage(getBytes(localMsgHash));
//@ts-ignore
await expect(mall.connect(otherAccount).buy(otherAccount.address, orderId, ft.target, amount, now, nonce, signature)).to.be.revertedWith("currency is not supported");
});
})
})