From fd5f2cbf12169e7bd9533cfe274a18417c61dc84 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 26 Oct 2023 19:05:18 +0200 Subject: [PATCH 1/2] Add initial impl of the unstaking Add `unstake` function to the `TokenStaking` contract - this function reduces stake amount by the provided amount and withdraws tokens to the owner. --- core/contracts/staking/TokenStaking.sol | 16 ++++++++++ core/contracts/test/TestToken.sol | 2 +- core/test/staking/TokenStaking.test.ts | 41 +++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/core/contracts/staking/TokenStaking.sol b/core/contracts/staking/TokenStaking.sol index 9866ce02b..bb66089c0 100644 --- a/core/contracts/staking/TokenStaking.sol +++ b/core/contracts/staking/TokenStaking.sol @@ -18,6 +18,7 @@ contract TokenStaking is IReceiveApproval { mapping(address => uint256) public balanceOf; event Staked(address indexed staker, uint256 amount); + event Unstaked(address indexed staker, uint256 amount); constructor(IERC20 _token) { require( @@ -51,6 +52,21 @@ contract TokenStaking is IReceiveApproval { _stake(msg.sender, amount); } + /// @notice Reduces stake amount by the provided amount and + /// withdraws tokens to the owner. + /// @param amount Amount to unstake and withdraw. + function unstake(uint256 amount) external { + require((amount > 0), "Amount can not be zero"); + + uint256 balance = balanceOf[msg.sender]; + require(balance >= amount, "Insufficient funds"); + + balanceOf[msg.sender] -= amount; + + emit Unstaked(msg.sender, amount); + token.safeTransfer(msg.sender, amount); + } + /// @notice Returns minimum amount of staking tokens to participate in /// protocol. function minimumStake() public pure returns (uint256) { diff --git a/core/contracts/test/TestToken.sol b/core/contracts/test/TestToken.sol index 943d20bd6..d7cc68b8c 100644 --- a/core/contracts/test/TestToken.sol +++ b/core/contracts/test/TestToken.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.20; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "../shared/IReceiveApproval.sol"; diff --git a/core/test/staking/TokenStaking.test.ts b/core/test/staking/TokenStaking.test.ts index c6b458bf9..1da165bce 100644 --- a/core/test/staking/TokenStaking.test.ts +++ b/core/test/staking/TokenStaking.test.ts @@ -95,4 +95,45 @@ describe("TokenStaking", () => { }) }) }) + + describe("unstaking", () => { + const amountToStake = WeiPerEther * 10n + + beforeEach(async () => { + // Stake tokens. + await token + .connect(tokenHolder) + .approveAndCall(await tokenStaking.getAddress(), amountToStake, "0x") + }) + + it("should unstake tokens", async () => { + const staker = await tokenHolder.getAddress() + const stakingBalance = await tokenStaking.balanceOf(staker) + const balanceBeforeUnstaking = await token.balanceOf(staker) + + await expect(tokenStaking.connect(tokenHolder).unstake(stakingBalance)) + .to.emit(tokenStaking, "Unstaked") + .withArgs(staker, stakingBalance) + + expect(await token.balanceOf(staker)).to.be.equal( + balanceBeforeUnstaking + stakingBalance, + ) + expect(await tokenStaking.balanceOf(staker)).to.be.eq(0) + }) + + it("should revert if the unstaked amount is equal 0", async () => { + await expect( + tokenStaking.connect(tokenHolder).unstake(0), + ).to.be.revertedWith("Amount can not be zero") + }) + + it("should revert if the user wants to unstake more tokens than currently staked", async () => { + const staker = await tokenHolder.getAddress() + const stakingBalance = await tokenStaking.balanceOf(staker) + + await expect( + tokenStaking.connect(tokenHolder).unstake(stakingBalance + 10n), + ).to.be.revertedWith("Insufficient funds") + }) + }) }) From 65baaacbb4e65f272f445c3cef7957e2d6079cca Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Fri, 3 Nov 2023 09:45:18 +0100 Subject: [PATCH 2/2] Refactor staking contract Create `Staker` struct that stores info about staker. Use this struct in `stakers` map instead of `balanceOf` map. We need to know when user staked their tokens because we want to add unstaking period - it measn the unstaking can be called when unstaking period passed since the stake has been deposited. --- core/contracts/staking/TokenStaking.sol | 20 +++++++++++++++----- core/test/staking/TokenStaking.test.ts | 18 +++++++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/core/contracts/staking/TokenStaking.sol b/core/contracts/staking/TokenStaking.sol index bb66089c0..d62b7ae69 100644 --- a/core/contracts/staking/TokenStaking.sol +++ b/core/contracts/staking/TokenStaking.sol @@ -13,9 +13,14 @@ import "../shared/IReceiveApproval.sol"; contract TokenStaking is IReceiveApproval { using SafeERC20 for IERC20; + struct Staker { + uint256 balance; + uint256 startStakingTimestamp; + } + IERC20 internal immutable token; - mapping(address => uint256) public balanceOf; + mapping(address => Staker) public stakers; event Staked(address indexed staker, uint256 amount); event Unstaked(address indexed staker, uint256 amount); @@ -58,10 +63,12 @@ contract TokenStaking is IReceiveApproval { function unstake(uint256 amount) external { require((amount > 0), "Amount can not be zero"); - uint256 balance = balanceOf[msg.sender]; - require(balance >= amount, "Insufficient funds"); + Staker storage staker = stakers[msg.sender]; + + require(staker.balance > 0, "Nothing to unstake"); + require(staker.balance >= amount, "Insufficient funds"); - balanceOf[msg.sender] -= amount; + staker.balance -= amount; emit Unstaked(msg.sender, amount); token.safeTransfer(msg.sender, amount); @@ -87,7 +94,10 @@ contract TokenStaking is IReceiveApproval { require(amount <= maximumStake(), "Amount is greater than maxium"); require(staker != address(0), "Can not be the zero address"); - balanceOf[staker] += amount; + Staker storage stakerStruct = stakers[staker]; + stakerStruct.balance += amount; + /* solhint-disable-next-line not-rely-on-time */ + stakerStruct.startStakingTimestamp = block.timestamp; // TODO: Mint stBTC token. emit Staked(staker, amount); diff --git a/core/test/staking/TokenStaking.test.ts b/core/test/staking/TokenStaking.test.ts index 1da165bce..ed9d24040 100644 --- a/core/test/staking/TokenStaking.test.ts +++ b/core/test/staking/TokenStaking.test.ts @@ -49,9 +49,9 @@ describe("TokenStaking", () => { await expect(tokenStaking.connect(tokenHolder).stake(amountToStake)) .to.emit(tokenStaking, "Staked") .withArgs(tokenHolderAddress, amountToStake) - expect(await tokenStaking.balanceOf(tokenHolderAddress)).to.be.eq( - amountToStake, - ) + expect( + (await tokenStaking.stakers(tokenHolderAddress)).balance, + ).to.be.eq(amountToStake) expect(await token.balanceOf(tokenHolderAddress)).to.be.eq( tokenBalanceBeforeStake - amountToStake, ) @@ -86,9 +86,9 @@ describe("TokenStaking", () => { ) .to.emit(tokenStaking, "Staked") .withArgs(tokenHolderAddress, amountToStake) - expect(await tokenStaking.balanceOf(tokenHolderAddress)).to.be.eq( - amountToStake, - ) + expect( + (await tokenStaking.stakers(tokenHolderAddress)).balance, + ).to.be.eq(amountToStake) expect(await token.balanceOf(tokenHolderAddress)).to.be.eq( tokenBalanceBeforeStake - amountToStake, ) @@ -108,7 +108,7 @@ describe("TokenStaking", () => { it("should unstake tokens", async () => { const staker = await tokenHolder.getAddress() - const stakingBalance = await tokenStaking.balanceOf(staker) + const stakingBalance = (await tokenStaking.stakers(staker)).balance const balanceBeforeUnstaking = await token.balanceOf(staker) await expect(tokenStaking.connect(tokenHolder).unstake(stakingBalance)) @@ -118,7 +118,7 @@ describe("TokenStaking", () => { expect(await token.balanceOf(staker)).to.be.equal( balanceBeforeUnstaking + stakingBalance, ) - expect(await tokenStaking.balanceOf(staker)).to.be.eq(0) + expect((await tokenStaking.stakers(staker)).balance).to.be.eq(0) }) it("should revert if the unstaked amount is equal 0", async () => { @@ -129,7 +129,7 @@ describe("TokenStaking", () => { it("should revert if the user wants to unstake more tokens than currently staked", async () => { const staker = await tokenHolder.getAddress() - const stakingBalance = await tokenStaking.balanceOf(staker) + const stakingBalance = (await tokenStaking.stakers(staker)).balance await expect( tokenStaking.connect(tokenHolder).unstake(stakingBalance + 10n),