Skip to content

Commit

Permalink
feat: add staking contract
Browse files Browse the repository at this point in the history
  • Loading branch information
0xibs committed Sep 9, 2024
1 parent 579ba36 commit bad2b60
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
212 changes: 212 additions & 0 deletions src/Staking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

interface IERC20 {
function balanceOf(address account) external view returns (uint256);

function transfer(
address recipient,
uint256 amount
) external returns (bool);

function allowance(
address owner,
address spender
) external view returns (uint256);

function approve(address spender, uint256 amount) external returns (bool);

function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
}

contract StakingContract {
struct StakeDetail {
uint256 timeStaked;
uint256 amount;
bool status;
}

mapping(address => StakeDetail) public stakers;
address public bwcErc20TokenAddress;
address public receiptTokenAddress;
address public rewardTokenAddress;
uint256 public totalStaked;

//////////////////
// CONSTANTS
//////////////////
uint256 constant MIN_TIME_BEFORE_WITHDRAW = 240; // Minimum time (in seconds) before tokens can be withdrawn. Equivalent to 4 minutes.
uint256 constant MULTIPLIER = 2; //

//////////////////
// EVENTS
//////////////////
event TokenStaked(address indexed staker, uint256 amount, uint256 time);
event TokenWithdraw(address indexed staker, uint256 amount, uint256 time);

//////////////////
// CONSTRUCTOR
//////////////////
constructor(
address _bwcErc20TokenAddress,
address _receiptTokenAddress,
address _rewardTokenAddress
) {
bwcErc20TokenAddress = _bwcErc20TokenAddress;
receiptTokenAddress = _receiptTokenAddress;
rewardTokenAddress = _rewardTokenAddress;
}

//////////////////
// STAKE FUNCTION
//////////////////
function stake(uint256 amount) external returns (bool) {
require(msg.sender != address(0), "STAKE: Address zero not allowed");
require(amount > 0, "STAKE: Zero amount not allowed");

IERC20 bwcToken = IERC20(bwcErc20TokenAddress);
IERC20 receiptToken = IERC20(receiptTokenAddress);

// Staker have enough token to stake
require(
bwcToken.balanceOf(msg.sender) >= amount,
"STAKE: Insufficient funds"
);

// Contract has enough receipt token to send to staker
require(
receiptToken.balanceOf(address(this)) >= amount,
"STAKE: Low contract receipt token balance"
);

// Staker has approved enough tokens to be staked
require(
bwcToken.allowance(msg.sender, address(this)) >= amount,
"STAKE: Amount not allowed"
);

StakeDetail storage stakeDetail = stakers[msg.sender];

stakeDetail.amount += amount;
stakeDetail.timeStaked = block.timestamp;
stakeDetail.status = true;

// Transfer stake token from Staker to contract
require(
bwcToken.transferFrom(msg.sender, address(this), amount),
"STAKE: Transfer failed"
);

// Increase total stake amount of Staker
totalStaked += amount;

// Transfer receipt token from contract to Staker
require(
receiptToken.transfer(msg.sender, amount),
"STAKE: Receipt token transfer failed"
);

emit TokenStaked(msg.sender, amount, block.timestamp);
return true;
}

//////////////////
// WITHDRAW FUNCTION
//////////////////
function withdraw(uint256 amount) external returns (bool) {
StakeDetail storage stakeDetail = stakers[msg.sender];

require(msg.sender != address(0), "WITHDRAW: Address zero not allowed");
require(amount > 0, "WITHDRAW: Zero amount not allowed");
require(
stakeDetail.amount >= amount,
"WITHDRAW: Withdraw amount not allowed"
);
require(
isTimeToWithdraw(stakeDetail.timeStaked),
"WITHDRAW: Not yet time to withdraw"
);

IERC20 bwcToken = IERC20(bwcErc20TokenAddress);
IERC20 receiptToken = IERC20(receiptTokenAddress);
IERC20 rewardToken = IERC20(rewardTokenAddress);

uint256 withdrawAmount = amount * MULTIPLIER;

require(
rewardToken.balanceOf(address(this)) >= withdrawAmount,
"WITHDRAW: Insufficient reward token balance"
);
require(
bwcToken.balanceOf(address(this)) >= amount,
"WITHDRAW: Insufficient BWC token balance"
);
require(
receiptToken.allowance(msg.sender, address(this)) >= amount,
"WITHDRAW: Receipt token allowance too low"
);

stakeDetail.amount -= amount;

require(
receiptToken.transferFrom(msg.sender, address(this), amount),
"WITHDRAW: Receipt token transfer failed"
);
require(
rewardToken.transfer(msg.sender, withdrawAmount),
"WITHDRAW: Reward token transfer failed"
);
require(
bwcToken.transfer(msg.sender, amount),
"WITHDRAW: BWC token transfer failed"
);

totalStaked -= amount;

emit TokenWithdraw(msg.sender, amount, block.timestamp);
return true;
}

//////////////////
// VIEW FUNCTIONS
//////////////////
function getStakeBalance(address staker) external view returns (uint256) {
return stakers[staker].amount;
}

function getNextWithdrawTime(
address staker
) external view returns (uint256) {
return
stakers[staker].timeStaked +
MIN_TIME_BEFORE_WITHDRAW -
block.timestamp;
}

function getTotalStake() external view returns (uint256) {
return totalStaked;
}

function getBwcTokenAddress() external view returns (address) {
return bwcErc20TokenAddress;
}

function getRewardTokenAddress() external view returns (address) {
return rewardTokenAddress;
}

function getReceiptTokenAddress() external view returns (address) {
return receiptTokenAddress;
}

//////////////////
// INTERNAL FUNCTION
//////////////////
function isTimeToWithdraw(uint256 stakeTime) internal view returns (bool) {
return block.timestamp >= stakeTime + MIN_TIME_BEFORE_WITHDRAW;
}
}
13 changes: 13 additions & 0 deletions test/ERC20.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ contract ERC20ContractTest is Test {
address ownerAddress = address(0x0101);
address randomAddress = address(0x3892);

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);

error InvalidRecipient();

function setUp() public {
Expand Down Expand Up @@ -58,6 +65,9 @@ contract ERC20ContractTest is Test {
);
// Set msg.sender to `ownerAddress`
vm.prank(ownerAddress);

vm.expectEmit(true, true, false, true);
emit Transfer(address(0), randomAddress, mintAmount);
// Mint 1000 tokens to random address
erc20Contract.mint(randomAddress, mintAmount);
uint256 totalSupplyAfterMint = erc20Contract.totalSupply();
Expand Down Expand Up @@ -86,6 +96,9 @@ contract ERC20ContractTest is Test {

// Set msg.sender to random address
vm.startPrank(randomAddress);

vm.expectEmit(true, true, false, true);
emit Approval(randomAddress, caller, amount);
// random address approves caller to spend `amount` tokens
erc20Contract.approve(caller, amount);
// Stop prank
Expand Down
38 changes: 38 additions & 0 deletions test/Staking.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Test, console} from "forge-std/Test.sol";
import {StakingContract} from "../src/Staking.sol";
import {ERC20} from "../src/ERC20.sol";

contract StakingContractTest is Test {
StakingContract public stakingContract;

ERC20 public bwcErc20TokenContract;
ERC20 public receiptTokenContract;
ERC20 public rewardTokenContract;

address bwcTokenAddress;
address receiptTokenAddress;
address rewardTokenAddress;

function setUp() public {
bwcErc20TokenContract = new ERC20("BlockheaderWeb3 Token", "BWC", 0);
receiptTokenContract = new ERC20("Receipt Token", "cBWC", 0);
rewardTokenContract = new ERC20("Reward Token", "wBWC", 0);

bwcTokenAddress = address(bwcErc20TokenContract);
receiptTokenAddress = address(receiptTokenContract);
rewardTokenAddress = address(rewardTokenContract);

stakingContract = new StakingContract(
bwcTokenAddress,
receiptTokenAddress,
rewardTokenAddress
);
}

function test_StakingContractDeployment() public view {
assertEq(stakingContract.bwcErc20TokenAddress(), bwcTokenAddress);
}
}

0 comments on commit bad2b60

Please sign in to comment.