Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
DepingMeng committed Jul 30, 2024
1 parent 4c94f47 commit 15f16dd
Show file tree
Hide file tree
Showing 19 changed files with 10,678 additions and 0 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Test

on:
push:
branches: ["main"]
pull_request:
types: [opened, synchronize]

jobs:
test:
name: Build and Test
timeout-minutes: 10
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "npm"
- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: "Add lint summary"
run: |
echo "## Lint results" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
- name: Compile
run: npm run compile

- name: Prettier
run: npm run prettier:check

- name: Test
run: npm run test

- name: Test
run: npm run coverage

- name: "Add test summary"
run: |
echo "## Test results" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
node_modules
.env
.openzeppelin
.idea

# Hardhat files
/cache
/artifacts

# TypeChain files
/typechain
/typechain-types

# solidity-coverage files
/coverage
/coverage.json

# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/

/args
17 changes: 17 additions & 0 deletions chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { vars } from "hardhat/config";

const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";

const owner = vars.has("OWNER") ? vars.get("OWNER") : ZERO_BYTES32;
const signer = vars.has("SIGNER") ? vars.get("SIGNER") : ZERO_BYTES32;

export const networks = {
hardhat: {
chainId: 31337,
},
gravity: {
chainId: 1625,
url: "https://rpc.gravity.xyz",
accounts: [owner, signer],
},
};
225 changes: 225 additions & 0 deletions contracts/LoyaltyPoint/LoyaltyPoint.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: Apache-2.0

/*
Copyright 2024 Galxe.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.8.24;

import {
AccessControlDefaultAdminRules
} from "@openzeppelin/contracts/access/extensions/AccessControlDefaultAdminRules.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { ERC20Pausable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import { ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { ILoyaltyPointHook } from "../interface/ILoyaltyPointHook.sol";

/// @title Galxe LoyaltyPoint Contract
/// @author Galxe Team
/// @notice This contract is used to create a loyalty point for a space.
/// @custom:security-contact [email protected]
contract LoyaltyPoint is ERC20, ERC20Permit, ERC20Pausable, AccessControlDefaultAdminRules {
error InvalidAddress();
error TransferNotAllow();
error ParamsLengthMismatch();
error PermissionDenied();
error SetTransferableNotAllowed();
error HookExecutionFailed();
error IndexOutOfRange();

modifier whenTransferable() {
if (!transferable()) {
revert TransferNotAllow();
}
_;
}

string private _name;
string private _symbol;
bool private _transferable;
ILoyaltyPointHook[] private _hooks;

bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

constructor(
string memory name_,
string memory symbol_,
address initialAdmin_,
address[] memory minters_
) ERC20(name_, symbol_) ERC20Permit(name_) AccessControlDefaultAdminRules(0, initialAdmin_) {
_transferable = false;
_name = super.name();
_symbol = super.symbol();
if (initialAdmin_ == address(0)) {
revert InvalidAddress();
}
for (uint256 i = 0; i < minters_.length; i++) {
if (minters_[i] == address(0)) {
revert InvalidAddress();
}
}
for (uint256 i = 0; i < minters_.length; i++) {
_grantRole(MINTER_ROLE, minters_[i]);
}
}

/// @notice Pauses the contract.
function pause() public onlyRole(DEFAULT_ADMIN_ROLE) {
_pause();
}

/// @notice Unpauses the contract.
function unpause() public onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
/// @notice Returns the name of the token.
/// @dev This is a custom function that overrides the OpenZeppelin function.
function name() public view override returns (string memory) {
return _name;
}

/// @notice Sets the name of the token.
/// @dev This gives the owner the ability to change the name of the token.
function setName(string calldata newName) public whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {
_name = newName;
}

/// @notice Returns the symbol of the token.
/// @dev This is a custom function that overrides the OpenZeppelin function.
function symbol() public view override returns (string memory) {
return _symbol;
}

/// @notice Sets the symbol of the token.
/// @dev This gives the owner the ability to change the name of the token.
function setSymbol(string calldata newSymbol) public whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {
_symbol = newSymbol;
}

/// @notice Returns the transferable status of the token.
function transferable() public view returns (bool) {
return _transferable;
}

/// @notice Sets the transferable status of the token.
/// @dev This gives the owner or space manager the ability to change the transferable status of the token.
function setTransferable(bool newTransferable) public whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {
_transferable = newTransferable;
}

/// @notice Adds a hook to the contract
/// @param hook The hook to be added
function addHook(ILoyaltyPointHook hook) public onlyRole(DEFAULT_ADMIN_ROLE) {
if (address(hook) == address(0)) {
revert InvalidAddress();
}
_hooks.push(hook);
}

/// @notice Deletes a hook from the contract
function deleteHook(uint256 index) public onlyRole(DEFAULT_ADMIN_ROLE) {
if (index >= _hooks.length) {
revert IndexOutOfRange();
}
delete _hooks[index];
}

/// @notice Returns the hooks of the contract
function getHooks() public view returns (ILoyaltyPointHook[] memory) {
return _hooks;
}

/// @notice Mints point for a user by minter
/// @param _user The address of the user who needs point minted
/// @param _amount The amount of points being minted
function mint(address _user, uint256 _amount) public whenNotPaused onlyRole(MINTER_ROLE) {
if (_user == address(0)) {
revert InvalidAddress();
}
_mint(_user, _amount);
}

/// @notice Mints points for multiple users by minter
/// @param _users The addresses of the users who need points minted
/// @param _amounts The amount of points being minted
function mintBatch(
address[] calldata _users,
uint256[] calldata _amounts
) public whenNotPaused onlyRole(MINTER_ROLE) {
if (_users.length != _amounts.length) {
revert ParamsLengthMismatch();
}
for (uint256 i = 0; i < _users.length; i++) {
if (_users[i] == address(0)) {
revert InvalidAddress();
}
}
for (uint256 i = 0; i < _users.length; i++) {
_mint(_users[i], _amounts[i]);
}
}

/// @notice Burns point for a user by minter
/// @param _user The address of the user who needs point burned
/// @param _amount The amount of points being burned
function burn(address _user, uint256 _amount) public whenNotPaused onlyRole(MINTER_ROLE) {
if (_user == address(0)) {
revert InvalidAddress();
}
_burn(_user, _amount);
}

/// @notice Burns points for multiple users by minter
/// @param _users The addresses of the users who need points burned
/// @param _amounts The amounts of points being burned
function burnBatch(
address[] calldata _users,
uint256[] calldata _amounts
) public whenNotPaused onlyRole(MINTER_ROLE) {
if (_users.length != _amounts.length) {
revert ParamsLengthMismatch();
}
for (uint256 i = 0; i < _users.length; i++) {
if (_users[i] == address(0)) {
revert InvalidAddress();
}
}
for (uint256 i = 0; i < _users.length; i++) {
_burn(_users[i], _amounts[i]);
}
}

/// @notice Transfers tokens from one account to another
function transfer(address to, uint256 value) public override whenNotPaused whenTransferable returns (bool) {
return super.transfer(to, value);
}

function transferFrom(
address from,
address to,
uint256 value
) public override whenNotPaused whenTransferable returns (bool) {
return super.transferFrom(from, to, value);
}

// Overrides required by Solidity.
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Pausable) {
for (uint256 i = 0; i < _hooks.length; i++) {
if (address(_hooks[i]) != address(0) && !_hooks[i].onUpdate(from, to, value)) {
revert HookExecutionFailed();
}
}
super._update(from, to, value);
}
}
60 changes: 60 additions & 0 deletions contracts/LoyaltyPoint/LoyaltyPointFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0

/*
Copyright 2024 Galxe.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.8.24;

import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol";
import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol";
import { LoyaltyPoint } from "./LoyaltyPoint.sol";

/// @title LoyaltyPoint Factory Contract
/// @author Galxe Team
/// @notice This contract is used to create a loyalty point contract
/// @custom:security-contact [email protected]
contract LoyaltyPointFactory is Ownable2Step, Pausable {
error InvalidAddress();

constructor(address _initialAdmin) Ownable(_initialAdmin) {
if (_initialAdmin == address(0)) {
revert InvalidAddress();
}
}

event CreateLoyaltyPoint(address loyaltyPoint, string name, string symbol, address initialAdmin, address[] minters);

/// @notice Pauses the contract.
function pause() public onlyOwner {
_pause();
}

/// @notice Unpauses the contract.
function unpause() public onlyOwner {
_unpause();
}

function createLoyaltyPoint(
string calldata _name,
string calldata _symbol,
address _initialAdmin,
address[] memory _minters
) external whenNotPaused returns (address contractAddr) {
bytes32 salt = keccak256(abi.encodePacked(_name, _symbol));
LoyaltyPoint _contract = new LoyaltyPoint{ salt: salt }(_name, _symbol, _initialAdmin, _minters);
contractAddr = address(_contract);
emit CreateLoyaltyPoint(contractAddr, _name, _symbol, _initialAdmin, _minters);
}
}
14 changes: 14 additions & 0 deletions contracts/TestLoyaltyPointHook/LoyaltyPointHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @dev for testing purpose
contract LoyaltyPointHook {
bool private _success;
constructor(bool success) {
_success = success;
}

function onUpdate(address, address, uint256) public view returns (bool) {
return _success;
}
}
Loading

0 comments on commit 15f16dd

Please sign in to comment.