Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token Staking #11

Closed
wants to merge 9 commits into from
Closed
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
16 changes: 16 additions & 0 deletions core/contracts/shared/IReceiveApproval.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.8.20;

/// @notice An interface that should be implemented by contracts supporting
/// `approveAndCall`/`receiveApproval` pattern.
interface IReceiveApproval {
/// @notice Receives approval to spend tokens. Called as a result of
/// `approveAndCall` call on the token.
function receiveApproval(
address from,
uint256 amount,
address token,
bytes calldata extraData
) external;
}
80 changes: 80 additions & 0 deletions core/contracts/staking/TokenStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-only

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "../shared/IReceiveApproval.sol";

/// @title TokenStaking
/// @notice A token staking contract for a specified standard ERC20 token. A
/// holder of the specified token can stake its tokens to this contract
/// and recover the stake after undelegation period is over.
contract TokenStaking is IReceiveApproval {
using SafeERC20 for IERC20;

IERC20 internal immutable token;

mapping(address => uint256) public balanceOf;

event Staked(address indexed staker, uint256 amount);

constructor(IERC20 _token) {
require(
address(_token) != address(0),
"Token can not be the zero address"
);

token = _token;
}

/// @notice Receives approval of token transfer and stakes the approved
/// amount or adds the approved amount to an existing stake.
/// @dev Requires that the provided token contract be the same one linked to
/// this contract.
/// @param from The owner of the tokens who approved them to transfer.
/// @param amount Approved amount for the transfer and stake.
/// @param _token Token contract address.
function receiveApproval(
address from,
uint256 amount,
address _token,
bytes calldata
) external override {
require(_token == address(token), "Unrecognized token");
_stake(from, amount);
}

/// @notice Stakes the owner's tokens in the staking contract.
/// @param amount Approved amount for the transfer and stake.
function stake(uint256 amount) external {
_stake(msg.sender, amount);
}

/// @notice Returns minimum amount of staking tokens to participate in
/// protocol.
function minimumStake() public pure returns (uint256) {
// TODO: Fetch this param from "parameters" contract that stores
// governable params.
return 1;
}

/// @notice Returns maximum amount of staking tokens.
function maximumStake() public pure returns (uint256) {
// TODO: Fetch this param from "parameters" contract that stores
// governable params.
return 100 ether;
}

function _stake(address staker, uint256 amount) private {
require(amount >= minimumStake(), "Amount is less than minimum");
require(amount <= maximumStake(), "Amount is greater than maxium");
require(staker != address(0), "Can not be the zero address");

balanceOf[staker] += amount;

// TODO: Mint stBTC token.
emit Staked(staker, amount);
token.safeTransferFrom(staker, address(this), amount);
}
}
31 changes: 31 additions & 0 deletions core/contracts/test/TestToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity 0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../shared/IReceiveApproval.sol";

contract Token is ERC20 {
constructor() ERC20("Test Token", "TEST") {}

function mint(address account, uint256 value) external {
_mint(account, value);
}

function approveAndCall(
address spender,
uint256 amount,
bytes memory extraData
) external returns (bool) {
if (approve(spender, amount)) {
IReceiveApproval(spender).receiveApproval(
msg.sender,
amount,
address(this),
extraData
);
return true;
}
return false;
}
}
2 changes: 1 addition & 1 deletion core/deploy/01_deploy_acre.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { HardhatRuntimeEnvironment } from "hardhat/types"
import type { DeployFunction } from "hardhat-deploy/types"

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => {
const { deployments } = hre
const { log } = deployments

Expand Down
3 changes: 3 additions & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@
"ts-node": ">=8.0.0",
"typechain": "^8.1.0",
"typescript": ">=4.5.0"
},
"dependencies": {
"@openzeppelin/contracts": "^5.0.0"
}
}
98 changes: 98 additions & 0 deletions core/test/staking/TokenStaking.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"
import { ethers } from "hardhat"
import { expect } from "chai"
import { WeiPerEther } from "ethers"
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"
import type { Token, TokenStaking } from "../../typechain"

async function tokenStakingFixture() {
const [_, tokenHolder] = await ethers.getSigners()
const StakingToken = await ethers.getContractFactory("Token")
const token = await StakingToken.deploy()

const amountToMint = WeiPerEther * 10000n

token.mint(tokenHolder, amountToMint)

const TokenStaking = await ethers.getContractFactory("TokenStaking")
const tokenStaking = await TokenStaking.deploy(await token.getAddress())

return { tokenStaking, token, tokenHolder }
}

describe("TokenStaking", () => {
let tokenStaking: TokenStaking
let token: Token
let tokenHolder: HardhatEthersSigner

beforeEach(async () => {
;({ tokenStaking, token, tokenHolder } =
await loadFixture(tokenStakingFixture))
})

describe("staking", () => {
const amountToStake = WeiPerEther * 10n

describe("when staking via staking contract directly", () => {
beforeEach(async () => {
// Infinite approval for staking contract.
await token
.connect(tokenHolder)
.approve(await tokenStaking.getAddress(), ethers.MaxUint256)
})

it("should stake tokens", async () => {
const tokenHolderAddress = await tokenHolder.getAddress()
const tokenBalanceBeforeStake =
await token.balanceOf(tokenHolderAddress)

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 token.balanceOf(tokenHolderAddress)).to.be.eq(
tokenBalanceBeforeStake - amountToStake,
)
})

it("should revert if the staked amount is less than required minimum", async () => {
await expect(
tokenStaking.connect(tokenHolder).stake(0),
).to.be.revertedWith("Amount is less than minimum")
})

it("should revert if the staked amount is grater than maxium stake amount", async () => {
const maxAmount = await tokenStaking.maximumStake()

await expect(
tokenStaking.connect(tokenHolder).stake(maxAmount + 1n),
).to.be.revertedWith("Amount is greater than maxium")
})
})

describe("when staking via staking token using approve and call pattern", () => {
it("should stake tokens", async () => {
const tokenHolderAddress = await tokenHolder.getAddress()
const tokenBalanceBeforeStake =
await token.balanceOf(tokenHolderAddress)
const tokenStakingAddress = await tokenStaking.getAddress()

await expect(
token
.connect(tokenHolder)
.approveAndCall(tokenStakingAddress, amountToStake, "0x"),
)
.to.emit(tokenStaking, "Staked")
.withArgs(tokenHolderAddress, amountToStake)
expect(await tokenStaking.balanceOf(tokenHolderAddress)).to.be.eq(
amountToStake,
)
expect(await token.balanceOf(tokenHolderAddress)).to.be.eq(
tokenBalanceBeforeStake - amountToStake,
)
})
})
})
})
5 changes: 5 additions & 0 deletions core/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,11 @@
resolved "https://registry.yarnpkg.com/@nomiclabs/hardhat-waffle/-/hardhat-waffle-2.0.6.tgz#d11cb063a5f61a77806053e54009c40ddee49a54"
integrity sha512-+Wz0hwmJGSI17B+BhU/qFRZ1l6/xMW82QGXE/Gi+WTmwgJrQefuBs1lIf7hzQ1hLk6hpkvb/zwcNkpVKRYTQYg==

"@openzeppelin/contracts@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.0.tgz#ee0e4b4564f101a5c4ee398cd4d73c0bd92b289c"
integrity sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw==

"@openzeppelin/defender-admin-client@^1.48.0":
version "1.49.0"
resolved "https://registry.yarnpkg.com/@openzeppelin/defender-admin-client/-/defender-admin-client-1.49.0.tgz#ed07318ccba10ac8a8a33cf594fc18b7ab5889f9"
Expand Down
Loading