diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e33d4c1 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PK= \ No newline at end of file diff --git a/.gitignore b/.gitignore index c16453e..de7c678 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ artifacts/contracts/DragonswapV2Staker.sol/DragonswapV2Staker.dbg.json artifacts/contracts/interfaces/**/*.dbg.json !artifacts/contracts/libraries artifacts/contracts/libraries/**/*.dbg.json + +# env +.env diff --git a/.nvmrc b/.nvmrc index 48082f7..3cacc0b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12 +12 \ No newline at end of file diff --git a/config.ts b/config.ts new file mode 100644 index 0000000..dd9fecf --- /dev/null +++ b/config.ts @@ -0,0 +1,31 @@ +export default { + dragonswapV2Staker: { + mainnet: "0x72c0cd98d21ee3263D375437b4FDAC097b596dD6", + testnet: "", + }, + refundee: { + mainnet: "", + testnet: "", + }, + pool: { + mainnet: "", + testnet: "", + }, + rewardToken: { + mainnet: "", + testnet: "", + }, + startTime: { + mainnet: 0, + testnet: 0, + }, + endTime: { + mainnet: 0, + testnet: 0, + }, + rewardAmount: { + mainnet: 0, // Amount is converted to decimals of the reward token in the script + testnet: 0, + }, + }; + \ No newline at end of file diff --git a/contracts/mocks/Token.sol b/contracts/mocks/Token.sol new file mode 100644 index 0000000..faca233 --- /dev/null +++ b/contracts/mocks/Token.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.6; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +contract Token is ERC20 { + uint8 private tokenDecimals; + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) ERC20(_name, _symbol) { + tokenDecimals = uint8(_decimals); + _mint(msg.sender, 100_000_000 * 10**_decimals); + } + + function decimals() public view virtual override returns (uint8) { + return tokenDecimals; + } +} diff --git a/deployments/addresses.json b/deployments/addresses.json new file mode 100644 index 0000000..e69de29 diff --git a/deployments/incentives.json b/deployments/incentives.json new file mode 100644 index 0000000..e69de29 diff --git a/hardhat.config.ts b/hardhat.config.ts index 8a819be..31a4ca0 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -4,8 +4,11 @@ import '@nomiclabs/hardhat-waffle' import '@typechain/hardhat' import 'hardhat-contract-sizer' import { HardhatUserConfig } from 'hardhat/config' -import { SolcUserConfig } from 'hardhat/types' +import { SolcUserConfig, NetworkUserConfig } from 'hardhat/types' import 'solidity-coverage' +import "dotenv/config"; + +const accounts = process.env.PK ? [process.env.PK] : []; const DEFAULT_COMPILER_SETTINGS: SolcUserConfig = { version: '0.7.6', @@ -35,44 +38,25 @@ if (process.env.RUN_COVERAGE == '1') { } } +const seiTestnet: NetworkUserConfig = { + url: "https://evm-rpc-testnet.sei-apis.com", + chainId: 1328, + accounts: accounts, +}; + +const seiMainnet: NetworkUserConfig = { + url: "https://evm-rpc.sei-apis.com", + chainId: 1329, + accounts: accounts, +}; + const config: HardhatUserConfig = { networks: { hardhat: { allowUnlimitedContractSize: false, }, - mainnet: { - url: `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - ropsten: { - url: `https://ropsten.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - rinkeby: { - url: `https://rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - goerli: { - url: `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - kovan: { - url: `https://kovan.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - arbitrumRinkeby: { - url: `https://arbitrum-rinkeby.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - arbitrum: { - url: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - optimismKovan: { - url: `https://optimism-kovan.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - optimism: { - url: `https://optimism-mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - mumbai: { - url: `https://polygon-mumbai.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, - polygon: { - url: `https://polygon-mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`, - }, + mainnet: seiMainnet, + testnet: seiTestnet, }, solidity: { compilers: [DEFAULT_COMPILER_SETTINGS], diff --git a/package.json b/package.json index 4da9a39..6510d41 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "compile": "hardhat compile", "lint": "eslint . --ext .ts", "prettier:check": "prettier-check contracts/**/*.sol test/**/*.ts types/*.ts", + "prettier:write": "prettier --write contracts/**/*.sol test/**/*.ts types/*.ts", "size-contracts": "hardhat compile && hardhat size-contracts", "test": "hardhat test", "clear-cache": "rm -rf artifacts/ cache/ typechain/", diff --git a/scripts/createIncentive.ts b/scripts/createIncentive.ts new file mode 100644 index 0000000..68a85e9 --- /dev/null +++ b/scripts/createIncentive.ts @@ -0,0 +1,95 @@ +import { ethers, network, run } from "hardhat"; +import config from "../config"; +import { saveJson, jsons, sleep } from "./utils"; +import { parseUnits } from "ethers/lib/utils"; + +const wait = async () => { + await sleep(3000); +}; + +const main = async () => { + // Get network data from Hardhat config (see hardhat.config.ts). + const networkName = network.name; + + // Check if the network is supported. + if (networkName === "testnet" || networkName === "mainnet") { + console.log(`Creating incentive on ${networkName} network...`); + + const zeroAddress = ethers.constants.AddressZero; + const refundeeAddress = config.refundee[networkName]; + + // Check if the addresses in the config are set. + if (refundeeAddress === zeroAddress || refundeeAddress === null || refundeeAddress === "") { + throw new Error("Missing refundee address"); + } + + // Compile contracts. + await run("compile"); + console.log("Compiled contracts..."); + + const dragonswapV2StakerAddress = config.dragonswapV2Staker[networkName]; + + const dragonswapV2Staker = await ethers.getContractAt( + "DragonswapV2Staker", + dragonswapV2StakerAddress, + ); + + // Define incentive parameters + const rewardToken = config.rewardToken[networkName]; // Address of the reward token + const pool = config.pool[networkName]; // Address of the Dragonswap V2 pool + const startTime = config.startTime[networkName]; // Start time + const endTime = config.endTime[networkName]; // End time + const refundee = refundeeAddress; // Address to receive leftover rewards + + // Create the incentive key + const incentiveKey = { + rewardToken, + pool, + startTime, + endTime, + refundee + }; + + // Approve the DragonswapV2Staker contract to spend reward tokens + const rewardTokenContract = await ethers.getContractAt("Token", rewardToken); + const rewardTokenDecimals = await rewardTokenContract.decimals(); + const rewardAmount = parseUnits(config.rewardAmount[networkName].toString(), rewardTokenDecimals); + await rewardTokenContract.approve(dragonswapV2StakerAddress, rewardAmount); + + await wait(); + + // Create the incentive + const createIncentiveTx = await dragonswapV2Staker.createIncentive( + incentiveKey, + rewardAmount + ); + + const createIncentiveTxReceipt = await createIncentiveTx.wait(); + + console.log("Incentive created. Transaction hash:", createIncentiveTxReceipt.transactionHash); + + // Save the incentive details + saveJson( + jsons.incentives, + network.name, + "latestIncentive", + { + rewardToken, + pool, + startTime, + endTime, + refundee, + rewardAmount: rewardAmount.toString() + } + ); + } else { + console.log(`Creating incentive on ${networkName} network is not supported...`); + } +}; + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployMockToken.ts b/scripts/deployMockToken.ts new file mode 100644 index 0000000..1596514 --- /dev/null +++ b/scripts/deployMockToken.ts @@ -0,0 +1,31 @@ +import { saveJson, jsons } from './utils'; +import { ethers , network, run } from "hardhat"; + + +async function main() { + // Compile contracts. + await run("compile"); + console.log("Compiled contracts..."); + + const name = "MockToken"; + const symbol = "MTKN"; + const decimals = 6; + + const mockTokenFactory = await ethers.getContractFactory('Token'); + const mockToken = await mockTokenFactory.deploy(name, symbol, decimals); + await mockToken.deployed(); + console.log(`MockToken address: ${mockToken.address}`); + + saveJson(jsons.addresses, network.name, 'MockToken', mockToken.address); + + console.log('Done!'); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/scripts/endIncentiveFromConfig.ts b/scripts/endIncentiveFromConfig.ts new file mode 100644 index 0000000..d157fe8 --- /dev/null +++ b/scripts/endIncentiveFromConfig.ts @@ -0,0 +1,64 @@ +import { ethers, network, run } from "hardhat"; +import config from "../config"; +import { jsons, sleep, getJson } from "./utils"; + +const wait = async () => { + await sleep(3000); +}; + +const main = async () => { + // Get network data from Hardhat config + const networkName = network.name; + + // Check if the network is supported + if (networkName === "testnet" || networkName === "mainnet") { + console.log(`Ending incentive on ${networkName} network...`); + + // Compile contracts + await run("compile"); + console.log("Compiled contracts..."); + + const dragonswapV2StakerAddress = config.dragonswapV2Staker[networkName]; + + const dragonswapV2Staker = await ethers.getContractAt( + "DragonswapV2Staker", + dragonswapV2StakerAddress, + ); + + // Load the incentive to end details from config + const incentiveFromConfig = { + rewardToken: config.rewardToken[networkName], + pool: config.pool[networkName], + startTime: config.startTime[networkName], + endTime: config.endTime[networkName], + refundee: config.refundee[networkName] + }; + + await wait(); + // End the incentive + const endIncentiveTx = await dragonswapV2Staker.endIncentive(incentiveFromConfig); + + const endIncentiveTxReceipt = await endIncentiveTx.wait(); + + console.log("Incentive ended. Transaction hash:", endIncentiveTxReceipt.transactionHash); + + // Get the refund amount from the transaction logs + const refundEvent = endIncentiveTxReceipt.events?.find(e => e.event === "IncentiveEnded"); + const refundAmount = refundEvent?.args?.refund; + + const rewardTokenContract = await ethers.getContractAt("Token", incentiveFromConfig.rewardToken); + const rewardTokenDecimals = await rewardTokenContract.decimals(); + + console.log("Refund amount:", ethers.utils.formatUnits(refundAmount, rewardTokenDecimals)); + + } else { + console.log(`Ending incentive on ${networkName} network is not supported...`); + } +}; + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/endIncentiveLatest.ts b/scripts/endIncentiveLatest.ts new file mode 100644 index 0000000..70f2205 --- /dev/null +++ b/scripts/endIncentiveLatest.ts @@ -0,0 +1,67 @@ +import { ethers, network, run } from "hardhat"; +import config from "../config"; +import { jsons, sleep, getJson } from "./utils"; + +const wait = async () => { + await sleep(3000); +}; + +const main = async () => { + // Get network data from Hardhat config + const networkName = network.name; + + // Check if the network is supported + if (networkName === "testnet" || networkName === "mainnet") { + console.log(`Ending incentive on ${networkName} network...`); + + // Compile contracts + await run("compile"); + console.log("Compiled contracts..."); + + const dragonswapV2StakerAddress = config.dragonswapV2Staker[networkName]; + + const dragonswapV2Staker = await ethers.getContractAt( + "DragonswapV2Staker", + dragonswapV2StakerAddress, + ); + + // Load the latest incentive details from incentives.json + const latestIncentive = getJson(jsons.incentives)[networkName]["latestIncentive"]; + + // Create the incentive key + const incentiveKey = { + rewardToken: latestIncentive.rewardToken, + pool: latestIncentive.pool, + startTime: latestIncentive.startTime, + endTime: latestIncentive.endTime, + refundee: latestIncentive.refundee + }; + + await wait(); + // End the incentive + const endIncentiveTx = await dragonswapV2Staker.endIncentive(incentiveKey); + + const endIncentiveTxReceipt = await endIncentiveTx.wait(); + + console.log("Incentive ended. Transaction hash:", endIncentiveTxReceipt.transactionHash); + + // Get the refund amount from the transaction logs + const refundEvent = endIncentiveTxReceipt.events?.find(e => e.event === "IncentiveEnded"); + const refundAmount = refundEvent?.args?.refund; + + const rewardTokenContract = await ethers.getContractAt("Token", latestIncentive.rewardToken); + const rewardTokenDecimals = await rewardTokenContract.decimals(); + + console.log("Refund amount:", ethers.utils.formatUnits(refundAmount, rewardTokenDecimals)); + + } else { + console.log(`Ending incentive on ${networkName} network is not supported...`); + } +}; + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 0000000..2b0cf6f --- /dev/null +++ b/scripts/utils.ts @@ -0,0 +1,40 @@ +import * as fs from "fs"; +import * as path from "path"; + +const relativePath = "../deployments/"; + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +const jsons: Record = { + addresses: "addresses.json", + config: "config.json", + abis: "abis.json", + incentives: "incentives.json", +}; + +function getJson(file: string): Record { + let json: string; + try { + json = fs.readFileSync(path.join(__dirname, relativePath, file), "utf8"); + } catch (err) { + json = "{}"; + } + return JSON.parse(json); +} + +function saveJson( + file: string, + network: string, + key: string, + value: any, +): void { + const json = getJson(file); + json[network] = json[network] || {}; + json[network][key] = value; + fs.writeFileSync( + path.join(__dirname, relativePath, file), + JSON.stringify(json, null, " "), + ); +} + +export { jsons, getJson, saveJson, sleep };