Skip to content

Commit

Permalink
Add SlashThresholdChanged event
Browse files Browse the repository at this point in the history
  • Loading branch information
pouya-eghbali committed Mar 9, 2024
1 parent f1b1442 commit eea95ba
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 3 deletions.
2 changes: 2 additions & 0 deletions contracts/Staking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ contract UnchainedStaking is Ownable, IERC721Receiver, ReentrancyGuard {
event Extended(address user, uint256 unlock);
event StakeIncreased(address user, uint256 amount, uint256[] nftIds);
event BlsAddressChanged(address user, bytes32 from, bytes32 to);
event SlashThresholdChanged(uint256 from, uint256 to);

/**
* @dev Modifier to temporarily allow the contract to receive NFTs.
Expand Down Expand Up @@ -612,6 +613,7 @@ contract UnchainedStaking is Ownable, IERC721Receiver, ReentrancyGuard {
revert Forbidden();
}

emit SlashThresholdChanged(_slashThreshold, threshold);
_slashThreshold = threshold;
}

Expand Down
79 changes: 78 additions & 1 deletion docs/UnchainedStaking.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

> UnchainedStaking
TODO
This contract allows users to stake ERC20 tokens and ERC721 NFTs, offering functionalities to stake, unstake, extend stakes, and manage slashing in case of misbehavior. It implements an EIP-712 domain for secure off-chain signature verifications, enabling decentralized governance actions like voting or slashing without on-chain transactions for each vote. The contract includes a slashing mechanism where staked tokens can be slashed (removed from the stake) if the majority of voting power agrees on a misbehavior. Users can stake tokens and NFTs either as consumers or not, affecting their roles within the ecosystem, particularly in governance or voting processes.



Expand Down Expand Up @@ -87,6 +87,23 @@ function getChainId() external view returns (uint256)
|---|---|---|
| _0 | uint256 | The current chain ID. |

### getSlashThreshold

```solidity
function getSlashThreshold() external view returns (uint256)
```



*Returns the current threshold for slashing to occur. This represents the minimum percentage of total voting power that must agree on a slash for it to be executed.*


#### Returns

| Name | Type | Description |
|---|---|---|
| _0 | uint256 | The slashing threshold as a percentage of total voting power. |

### increaseStake

```solidity
Expand Down Expand Up @@ -213,6 +230,22 @@ function setBlsAddress(bytes32 blsAddress) external nonpayable
|---|---|---|
| blsAddress | bytes32 | The new BLS address to be set for the user. |

### setSlashThreshold

```solidity
function setSlashThreshold(uint256 threshold) external nonpayable
```



*Sets the minimum percentage of total voting power required to successfully execute a slash. Only callable by the contract owner. The threshold must be at least 51% to ensure a majority vote.*

#### Parameters

| Name | Type | Description |
|---|---|---|
| threshold | uint256 | The new slashing threshold as a percentage. |

### slash

```solidity
Expand Down Expand Up @@ -685,6 +718,23 @@ error LengthMismatch()



### NonceUsed

```solidity
error NonceUsed(uint256 index, uint256 nonce)
```





#### Parameters

| Name | Type | Description |
|---|---|---|
| index | uint256 | undefined |
| nonce | uint256 | undefined |

### NotConsumer

```solidity
Expand Down Expand Up @@ -744,6 +794,17 @@ error OwnableUnauthorizedAccount(address account)
|---|---|---|
| account | address | undefined |

### ReentrancyGuardReentrantCall

```solidity
error ReentrancyGuardReentrantCall()
```



*Unauthorized reentrant call.*


### SafeERC20FailedOperation

```solidity
Expand Down Expand Up @@ -781,6 +842,22 @@ error VotingPowerZero(uint256 index)



#### Parameters

| Name | Type | Description |
|---|---|---|
| index | uint256 | undefined |

### WrongAccused

```solidity
error WrongAccused(uint256 index)
```





#### Parameters

| Name | Type | Description |
Expand Down
26 changes: 26 additions & 0 deletions docs/elin/contracts/utils/ReentrancyGuard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ReentrancyGuard







*Contract module that helps prevent reentrant calls to a function. Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier available, which can be applied to functions to make sure there are no nested (reentrant) calls to them. Note that because there is a single `nonReentrant` guard, functions marked as `nonReentrant` may not call one another. This can be worked around by making those functions `private`, and then adding `external` `nonReentrant` entry points to them. TIP: If you would like to learn more about reentrancy and alternative ways to protect against it, check out our blog post https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].*



## Errors

### ReentrancyGuardReentrantCall

```solidity
error ReentrancyGuardReentrantCall()
```



*Unauthorized reentrant call.*



167 changes: 165 additions & 2 deletions test/Staking.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,166 @@
const { time } = require("@nomicfoundation/hardhat-network-helpers");
describe("UnchainedStaking", function () {
let staking, token, nft;
let owner, user1;

describe("Unchained Staking", function () {});
// FIXME
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();

// Deploy Mock ERC20 token
const Token = await ethers.getContractFactory("MockERC20");
token = await Token.deploy("MockToken", "MTK");
await token.deployed();

// Deploy Mock ERC721 NFT
const NFT = await ethers.getContractFactory("MockERC721");
nft = await NFT.deploy("MockNFT", "MNFT");
await nft.deployed();

// Deploy the Staking contract
const Staking = await ethers.getContractFactory("UnchainedStaking");
staking = await Staking.deploy(
token.address,
nft.address,
10,
owner.address,
"UnchainedStaking",
"1"
);
await staking.deployed();

// Mint tokens and NFT to user1
await token.mint(user1.address, ethers.utils.parseEther("1000"));
await nft.mint(user1.address, 1);

// Approve Staking contract to spend tokens and NFT
await token
.connect(user1)
.approve(staking.address, ethers.utils.parseEther("1000"));
await nft.connect(user1).setApprovalForAll(staking.address, true);
});

it("allows users to stake tokens and NFTs", async function () {
await expect(
staking
.connect(user1)
.stake(60 * 60 * 24, ethers.utils.parseEther("500"), [1], true)
)
.to.emit(staking, "Staked")
.withArgs(
user1.address,
ethers.constants.AnyNumber,
ethers.utils.parseEther("500"),
[1],
true
);

// Check balances and ownership
expect(await token.balanceOf(staking.address)).to.equal(
ethers.utils.parseEther("500")
);
expect(await nft.ownerOf(1)).to.equal(staking.address);
});

it("allows users to unstake after lock period", async function () {
await staking
.connect(user1)
.stake(1, ethers.utils.parseEther("500"), [1], true);

// Increase time to surpass the stake duration
await ethers.provider.send("evm_increaseTime", [2]);
await ethers.provider.send("evm_mine");

await expect(staking.connect(user1).unstake())
.to.emit(staking, "UnStaked")
.withArgs(user1.address, ethers.constants.AnyNumber, [1]);

// Check balances and ownership
expect(await token.balanceOf(user1.address)).to.equal(
ethers.utils.parseEther("500")
);
expect(await nft.ownerOf(1)).to.equal(user1.address);
});

it("rejects staking with zero amount", async function () {
await expect(
staking.connect(user1).stake(60 * 60 * 24, 0, [1], true)
).to.be.revertedWith("AmountZero()");
});

it("rejects staking with zero duration", async function () {
await expect(
staking.connect(user1).stake(0, ethers.utils.parseEther("500"), [1], true)
).to.be.revertedWith("DurationZero()");
});

it("rejects staking when already staked without unstaking", async function () {
await staking
.connect(user1)
.stake(60 * 60 * 24, ethers.utils.parseEther("100"), [1], true);
await expect(
staking
.connect(user1)
.stake(60 * 60 * 24, ethers.utils.parseEther("100"), [2], true)
).to.be.revertedWith("AlreadyStaked()");
});

it("rejects unstaking before duration expires", async function () {
await staking
.connect(user1)
.stake(60 * 60 * 24, ethers.utils.parseEther("500"), [1], true);

// Attempt to unstake immediately
await expect(staking.connect(user1).unstake()).to.be.revertedWith(
"NotUnlocked()"
);
});

it("successfully slashes a staker based on consensus", async function () {
// This is highly dependent on your slashing mechanism.
// The following is a very simplified version assuming a direct slash call can be made.

// Setup a scenario where `user1` can be slashed
// Note: The actual setup would depend on your contract's logic for slashing
await staking
.connect(user1)
.stake(60 * 60 * 24 * 365, ethers.utils.parseEther("500"), [1], true);

// Assume `owner` has the authority to slash directly for this test
await expect(
staking
.connect(owner)
.slash(
[user1.address],
[ethers.utils.parseEther("100")],
["incidentID"]
)
)
.to.emit(staking, "Slashed")
.withArgs(
user1.address,
owner.address,
ethers.utils.parseEther("100"),
ethers.constants.AnyNumber,
"incidentID"
);

// Verify the slash was successful
const postSlashStake = await staking.stakeOf(user1.address);
expect(postSlashStake.amount).to.equal(ethers.utils.parseEther("400"));
});

it("allows increasing the stake", async function () {
await staking
.connect(user1)
.stake(60 * 60 * 24, ethers.utils.parseEther("500"), [1], true);

// Increase stake
await staking
.connect(user1)
.increaseStake(ethers.utils.parseEther("500"), [2]);
const postIncreaseStake = await staking.stakeOf(user1.address);

expect(postIncreaseStake.amount).to.equal(ethers.utils.parseEther("1000"));
expect(postIncreaseStake.nftIds.length).to.equal(2);
});
});

0 comments on commit eea95ba

Please sign in to comment.