Skip to content
Merged
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
7 changes: 6 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@
"girlnext",
"Checksummed",
"checksummed",
"BMNFT"
"BMNFT",
"unstake",
"unstakes",
"Unstaked",
"funder",
"Fundings"
]
}
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,46 @@ N_LEVELS_URI_2=example.com \
forge script script/DeployMigrationNFT.s.sol --zksync --rpc-url https://sepolia.era.zksync.dev --zk-optimizer -i 1 --broadcast
```

## Deploying Staking contract
Allow users with at least 50,000 NODL (Dolphin level) to participate in a staking contract with the following characteristics:

### Functional Requirements
- Restricted access: Only users holding 50,000 NODL or more can stake.

- Staking cap: The contract accepts a maximum total of 5 million NODL per staker. Additionally, the contract has a global staking cap MAX_POOL_STAKE.

- Limited duration: Staking lasts the DURATION in seconds passed as a parameter.

- Guaranteed yield: Users receive a fixed reward, predefined in the contract, at the end of the staking period.

- Return: Once the staking period ends, both the staked tokens and the yield are returned to the user via a claim function.

### Deployment

```shell
DEPLOYER_PRIVATE_KEY=0x... \
GOV_ADDR=0x2D1941280530027B6bA80Af0e7bD8c2135783368 \
STAKE_TOKEN=0xb4B74C2BfeA877672B938E408Bae8894918fE41C \
MIN_STAKE=50000 \
MAX_TOTAL_STAKE=5000000 \
DURATION=3600 \
REWARD_RATE=12 \
REQUIRED_HOLDING_TOKEN=50000 \
npx hardhat deploy-zksync --script deploy_staking.dp.ts --network zkSyncSepoliaTestnet
```
- DEPLOYER_PRIVATE_KEY: Private key of the deploying account.
- GOV_ADDR: Address of the governance contract (receives admin, rewards-manager, and emergency-manager roles).
- STAKE_TOKEN: Address of the token to stake.
- MIN_STAKE: Minimum amount of tokens a user can stake (whole tokens, scaled by 1e18 in the script).
- MAX_TOTAL_STAKE: Maximum amount of tokens a single user can stake (whole tokens, scaled by 1e18).
- DURATION: Duration of the staking period, in seconds.
- REWARD_RATE: Reward rate as a whole-number percent of the staked amount, paid at the end of the period.
- REQUIRED_HOLDING_TOKEN: Minimum token balance a user must hold to participate (whole tokens, scaled by 1e18).

### Trust assumptions

The admin account (GOV_ADDR) holds the default-admin, rewards-manager, and emergency-manager roles. It can pause the contract (which blocks `claim`/`unstake`), toggle `unstakeAllowed` (which defaults to false, so before the period ends users can only exit once the admin enables it), and — while paused — call `emergencyWithdraw` to sweep the entire contract balance, including staked principal. Deployments intended for untrusted users should split these roles and/or place them behind a timelock or multisig.

## Scripts

### Checking on bridging proposals
Expand Down Expand Up @@ -242,3 +282,17 @@ Use all these artifacts on the contract verification page on Etherscan for your

- [L1 contracts](https://docs.zksync.io/zksync-era/environment/l1-contracts)
- [ZK stack addresses](https://docs.zksync.io/zk-stack/zk-chain-addresses)

## ZkSync CLI useful commands

# Approve

```shell
npx zksync-cli contract write --chain "zksync-sepolia" --contract "0xb4B74C2BfeA877672B938E408Bae8894918fE41C" --method "approve(address spender, uint256 amount)" --args "0x2D1941280530027B6bA80Af0e7bD8c2135783368" "1000000000000000000"
```

# Stake

```shell
npx zksync-cli contract write --chain "zksync-sepolia" --contract "0xb974a544128Bc7fAB3447D48cd6ad377D6F62EcF" --method "stake(uint256 amount)" --args "1000000000000000000"
```
75 changes: 75 additions & 0 deletions hardhat-deploy/deploy_staking.dp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Provider, Wallet } from "zksync-ethers";
import { Deployer } from "@matterlabs/hardhat-zksync";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import "@matterlabs/hardhat-zksync-node/dist/type-extensions";
import "@matterlabs/hardhat-zksync-verify/dist/src/type-extensions";
import * as dotenv from "dotenv";

import { deployContract } from "./utils";

dotenv.config();

function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}

module.exports = async function (hre: HardhatRuntimeEnvironment) {
const tokenAddress = requireEnv("STAKE_TOKEN");
const adminAddress = requireEnv("GOV_ADDR");
const minStakeAmount = requireEnv("MIN_STAKE");
const stakingPeriod = requireEnv("DURATION");
const rewardRate = requireEnv("REWARD_RATE");
const maxTotalStake = requireEnv("MAX_TOTAL_STAKE");
const requiredHoldingToken = requireEnv("REQUIRED_HOLDING_TOKEN");

// REWARD_RATE is an integer percent and DURATION is an integer number of seconds
if (!/^\d+$/.test(rewardRate)) {
throw new Error(`REWARD_RATE must be a whole-number percent, got "${rewardRate}"`);
}
if (!/^\d+$/.test(stakingPeriod)) {
throw new Error(`DURATION must be a whole number of seconds, got "${stakingPeriod}"`);
}

const rpcUrl = hre.network.config.url!;
const provider = new Provider(rpcUrl);
const wallet = new Wallet(requireEnv("DEPLOYER_PRIVATE_KEY"), provider);
const deployer = new Deployer(hre, wallet);

const constructorArgs = [
tokenAddress,
(BigInt(requiredHoldingToken) * BigInt(1e18)).toString(),
Number(rewardRate),
(BigInt(minStakeAmount) * BigInt(1e18)).toString(),
(BigInt(maxTotalStake) * BigInt(1e18)).toString(),
Number(stakingPeriod),
adminAddress,
];

const staking = await deployContract(deployer, "Staking", constructorArgs);
const contractAddress = await staking.getAddress();
console.log(`Staking contract deployed at ${contractAddress}`);
console.log(
`!!! Do not forget to grant token approval to Staking contract at ${contractAddress} !!!`
);

console.log("Starting contract verification...");
try {
await hre.run("verify:verify", {
address: contractAddress,
contract: "src/Staking.sol:Staking",
constructorArguments: constructorArgs,
});
console.log("Contract verified successfully!");
} catch (error: any) {
if (error.message.includes("Contract source code already verified")) {
console.log("Contract is already verified!");
} else {
console.error("Error verifying contract:", error);
throw error;
}
}
};
Loading
Loading