Skip to content

Commit

Permalink
Build SQTGift NFT contract (#309)
Browse files Browse the repository at this point in the history
* Build `SQTGift` NFT contract

* Add `SQTRedeem` contract

* Update `redeem` function

* Update gift contracts

* Add extra condition to redeem

* Add more events

* Update contracts/SQTGift.sol

Co-authored-by: Ian He <[email protected]>

* Update contracts/SQTGift.sol

Co-authored-by: Ian He <[email protected]>

* Update revert codes for `SQTGift`

* Update revert code for `SQTRedeem`

* remove redeem logic from SQTGift

* improve test

* clean up

* add sqtGift to ts

* add batchMint() and publish to testnet

---------

Co-authored-by: Ian He <[email protected]>
  • Loading branch information
mzxyz and ianhe8x authored Jan 4, 2024
1 parent e9757b0 commit e3568ac
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 5 deletions.
161 changes: 161 additions & 0 deletions contracts/SQTGift.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.15;

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';

import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/utils/introspection/ERC165CheckerUpgradeable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';

import './interfaces/ISQTGift.sol';

contract SQTGift is Initializable, OwnableUpgradeable, ERC721Upgradeable, ERC721URIStorageUpgradeable, ERC721EnumerableUpgradeable, ISQTGift {

uint256 public nextSeriesId;

/// @notice seriesId => GiftSeries
mapping(uint256 => GiftSeries) public series;

/// @notice account => seriesId => gift count
mapping(address => mapping(uint256 => uint8)) public allowlist;

/// @notice tokenId => Gift
mapping(uint256 => Gift) public gifts;

event AllowListAdded(address indexed account, uint256 indexed seriesId, uint8 amount);
event AllowListRemoved(address indexed account, uint256 indexed seriesId, uint8 amount);

event SeriesCreated(uint256 indexed seriesId, uint256 maxSupply, string tokenURI);
event SeriesActiveUpdated(uint256 indexed seriesId, bool active);

event GiftMinted(address indexed to, uint256 indexed seriesId, uint256 indexed tokenId, string tokenURI);

function initialize() external initializer {
__Ownable_init();
__ERC721_init("SQT Gift", "SQTG");
__ERC721URIStorage_init();
__ERC721Enumerable_init();
}

function batchAddToAllowlist(uint256[] calldata _seriesId, address[] calldata _address, uint8[] calldata _amount) public onlyOwner {
require(_seriesId.length == _address.length, 'SQG003');
require(_seriesId.length == _amount.length, 'SQG003');
for (uint256 i = 0; i < _seriesId.length; i++) {
addToAllowlist(_seriesId[i], _address[i], _amount[i]);
}
}

function addToAllowlist(uint256 _seriesId, address _address, uint8 _amount) public onlyOwner {
require(series[_seriesId].maxSupply > 0, "SQG001");
allowlist[_address][_seriesId] += _amount;

emit AllowListAdded(_address, _seriesId, _amount);
}

function removeFromAllowlist(uint256 _seriesId, address _address, uint8 _amount) public onlyOwner {
require(series[_seriesId].maxSupply > 0, "SQG001");
require(allowlist[_address][_seriesId] >= _amount, "SQG002");
allowlist[_address][_seriesId] -= _amount;

emit AllowListRemoved(_address, _seriesId, _amount);
}

function createSeries(
uint256 _maxSupply,
string memory _tokenURI
) external onlyOwner {
require(_maxSupply > 0, "SQG006");
series[nextSeriesId] = GiftSeries({
maxSupply: _maxSupply,
totalSupply: 0,
active: true,
tokenURI: _tokenURI
});

emit SeriesCreated(nextSeriesId, _maxSupply, _tokenURI);

nextSeriesId += 1;

}

function setSeriesActive(uint256 _seriesId, bool _active) external onlyOwner {
require(series[_seriesId].maxSupply > 0, "SQG001");
series[_seriesId].active = _active;

emit SeriesActiveUpdated(_seriesId, _active);
}

function setMaxSupply(uint256 _seriesId, uint256 _maxSupply) external onlyOwner {
require(_maxSupply > 0, "SQG006");
series[_seriesId].maxSupply = _maxSupply;
}

function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize) internal override(
ERC721Upgradeable,
ERC721EnumerableUpgradeable
) {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}

function supportsInterface(bytes4 interfaceId) public view override(
IERC165Upgradeable,
ERC721Upgradeable,
ERC721EnumerableUpgradeable,
ERC721URIStorageUpgradeable
) returns (bool) {
return interfaceId == type(ISQTGift).interfaceId || super.supportsInterface(interfaceId);
}

function tokenURI(uint256 tokenId) public view override(
ERC721Upgradeable,
ERC721URIStorageUpgradeable
) returns (string memory) {
return super.tokenURI(tokenId);
}

function _burn(uint256 tokenId) internal override(ERC721Upgradeable, ERC721URIStorageUpgradeable) {
super._burn(tokenId);
}

function _baseURI() internal view virtual override returns (string memory) {
return "ipfs://";
}

function mint(uint256 _seriesId) public {
GiftSeries memory giftSerie = series[_seriesId];
require(giftSerie.active, "SQG004");
require(allowlist[msg.sender][_seriesId] > 0, "SQG002");

require(giftSerie.totalSupply < giftSerie.maxSupply, "SQG005");
series[_seriesId].totalSupply += 1;

uint256 tokenId = totalSupply() + 1;
gifts[tokenId].seriesId = _seriesId;

_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, giftSerie.tokenURI);

allowlist[msg.sender][_seriesId]--;

emit GiftMinted(msg.sender, _seriesId, tokenId, giftSerie.tokenURI);
}

function batchMint(uint256 _seriesId) external {
GiftSeries memory giftSerie = series[_seriesId];
require(giftSerie.active, "SQG004");
uint8 allowAmount = allowlist[msg.sender][_seriesId];
require(allowAmount > 0, "SQG002");
for (uint256 i = 0; i < allowAmount; i++) {
mint(_seriesId);
}
}

function getSeries(uint256 tokenId) external view returns (uint256) {
return gifts[tokenId].seriesId;
}
}
68 changes: 68 additions & 0 deletions contracts/SQTRedeem.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.15;

import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol";
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';

import './interfaces/ISQTGift.sol';

contract SQTRedeem is Initializable, OwnableUpgradeable {
function initialize(address _sqtoken) external initializer {}
//
// address public sqtoken;
//
// bool public redeemable;
//
// mapping (address => bool) public allowlist;
//
// event SQTRedeemed(address indexed to, address nft, uint256 indexed tokenId, uint256 sqtValue);
//
// function initialize(address _sqtoken) external initializer {
// __Ownable_init();
//
// sqtoken = _sqtoken;
// }
//
// function desposit(uint256 amount) public onlyOwner {
// require(IERC20(sqtoken).transferFrom(msg.sender, address(this), amount), 'SQR001');
// }
//
// function withdraw(uint256 amount) public onlyOwner {
// require(IERC20(sqtoken).transfer(msg.sender, amount), 'SQR001');
// }
//
// function addToAllowlist(address _address) public onlyOwner {
// allowlist[_address] = true;
// }
//
// function removeFromAllowlist(address _address) public onlyOwner {
// allowlist[_address] = false;
// }
//
// function setRedeemable(bool _redeemable) external onlyOwner {
// redeemable = _redeemable;
// }
//
// function redeem(address nft, uint256 tokenId) public {
// require(redeemable, "SQR002");
// require(allowlist[nft], "SQR003");
//
// IERC165Upgradeable nftContract = IERC165Upgradeable(nft);
// require(nftContract.supportsInterface(type(ISQTGift).interfaceId), "SQR004");
//
// ISQTGift sqtGift = ISQTGift(nft);
// require(sqtGift.getGiftRedeemable(tokenId), "SQG005");
// require(sqtGift.ownerOf(tokenId) == msg.sender, "SQG006");
// uint256 sqtValue = sqtGift.getSQTRedeemableValue(tokenId);
// require(sqtValue > 0, "SQG007");
// sqtGift.afterTokenRedeem(tokenId);
//
// require(IERC20(sqtoken).transfer(msg.sender, sqtValue), "SQR001");
//
// emit SQTRedeemed(msg.sender, nft, tokenId, sqtValue);
// }
}
21 changes: 21 additions & 0 deletions contracts/interfaces/ISQTGift.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (C) 2020-2023 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.15;

import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";

struct GiftSeries {
uint256 maxSupply;
uint256 totalSupply;
bool active;
string tokenURI;
}

struct Gift {
uint256 seriesId;
}

interface ISQTGift is IERC721Upgradeable {
function getSeries(uint256 tokenId) external view returns (uint256);
}
9 changes: 9 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ task('publishChild', "verify and publish contracts on etherscan")
address: deployment.TokenExchange.address,
constructorArguments: [],
});
//SQTGift
await hre.run("verify:verify", {
address: deployment.SQTGift.address,
constructorArguments: [deployment.SQTGift.innerAddress, deployment.ProxyAdmin.address, []],
});
await hre.run("verify:verify", {
address: deployment.SQTGift.innerAddress,
constructorArguments: [],
});

} catch (err) {
console.log(err);
Expand Down
16 changes: 15 additions & 1 deletion publish/revertcode.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,19 @@
"TE001": "Token give balance must be greater than 0",
"TE002": "order not exist",
"TE003": "trade amount exceed order balance",
"PD001": "RootChainManager is undefined"
"PD001": "RootChainManager is undefined",
"SQG001": "Series not found",
"SQG002": "Not on allowlist",
"SQG003": "Invalid Batch Parameters",
"SQG004": "Series not active",
"SQG005": "Max gift supply reached",
"SQG006": "Max supply must be greater than 0",

"SQR001": "Failed to transfer SQT",
"SQR002": "Redeem not enabled",
"SQR003": "NFT not allowed to redeem",
"SQR004": "NFT does not support ISQTGift",
"SQR005": "Gift not redeemable",
"SQR006": "Not owner of token",
"SQR007": "No SQT to redeem"
}
8 changes: 7 additions & 1 deletion publish/testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"SQToken": {
"innerAddress": "",
"address": "0xAFD07FAB547632d574b38A72EDAE93fA23d1E7d7",
"address": "r",
"bytecodeHash": "b36d78daa299ef0902228a52e74c6016998ac240026c54624ba882e15d4a29cc",
"lastUpdate": "Mon, 11 Dec 2023 07:37:28 GMT"
},
Expand Down Expand Up @@ -188,6 +188,12 @@
"bytecodeHash": "58b29e3ecad69575fb0ea5f4699ff93df771c4ec78c857f0f6f711840f2192b3",
"lastUpdate": "Fri, 15 Dec 2023 06:11:39 GMT"
},
"SQTGift": {
"innerAddress": "0x05fC60d66d4386C14145c88f3b92Fb55642452c0",
"address": "0xCe008ea6ef4B7C5712B8B8DF7A4ca021859ab266",
"bytecodeHash": "3d7cdd1cbad232b4d9d6804bffbdf9f64804c89c0fab972e8a5839b6b51ee34a",
"lastUpdate": "Thu, 04 Jan 2024 02:58:39 GMT"
},
"Airdropper": {
"innerAddress": "",
"address": "0xD01AE239CFDf49d88F1c3ab6d2c82b7f411C71b1",
Expand Down
4 changes: 4 additions & 0 deletions scripts/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import {
PolygonDestination,
PolygonDestination__factory,
ChildERC20__factory,
SQTGift__factory,
SQTGift,
VTSQToken,
VTSQToken__factory,
} from '../src';
Expand Down Expand Up @@ -105,6 +107,7 @@ export type Contracts = {
consumerRegistry: ConsumerRegistry;
priceOracle: PriceOracle;
polygonDestination: PolygonDestination;
sqtGift: SQTGift;
};

export const UPGRADEBAL_CONTRACTS: Partial<Record<keyof typeof CONTRACTS, [{ bytecode: string }, FactoryContstructor]>> =
Expand Down Expand Up @@ -164,6 +167,7 @@ export const CONTRACT_FACTORY: Record<ContractName, FactoryContstructor> = {
PriceOracle: PriceOracle__factory,
ChildERC20: ChildERC20__factory,
PolygonDestination: PolygonDestination__factory,
SQTGift: SQTGift__factory,
};

export type Config = number | string | string[];
Expand Down
11 changes: 9 additions & 2 deletions scripts/deployContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
TokenExchange,
PolygonDestination,
RootChainManager__factory,
SQTGift,
Airdropper,
VTSQToken,
} from '../src';
Expand Down Expand Up @@ -436,13 +437,18 @@ export async function deployContracts(
initConfig: [10, 3600],
});

// delpoy PriceOracle contract
const sqtGift = await deployContract<SQTGift>('SQTGift', 'child', {
proxyAdmin,
initConfig: [],
});

//deploy Airdropper contract
const [settleDestination] = config['Airdropper'];
const airdropper = await deployContract<Airdropper>('Airdropper', 'child', { deployConfig: [settleDestination] });

// Register addresses on settings contract
// FIXME: failed to send this tx
logger?.info('🤞 Set token addresses');
logger?.info('🤞 Set settings addresses');
const txToken = await settings.setBatchAddress([
SQContracts.SQToken,
SQContracts.Staking,
Expand Down Expand Up @@ -510,6 +516,7 @@ export async function deployContracts(
tokenExchange,
priceOracle,
consumerRegistry,
sqtGift,
airdropper,
},
];
Expand Down
2 changes: 2 additions & 0 deletions src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import ChildERC20 from './artifacts/contracts/polygon/ChildERC20.sol/ChildERC20.
import Vesting from './artifacts/contracts/root/Vesting.sol/Vesting.json';
import VTSQToken from './artifacts/contracts/root/VTSQToken.sol/VTSQToken.json';
import PolygonDestination from './artifacts/contracts/root/PolygonDestination.sol/PolygonDestination.json';
import SQTGift from "./artifacts/contracts/SQTGift.sol/SQTGift.json";

export default {
Settings,
Expand Down Expand Up @@ -63,4 +64,5 @@ export default {
ConsumerRegistry,
ChildERC20,
PolygonDestination,
SQTGift,
};
Loading

0 comments on commit e3568ac

Please sign in to comment.