feat: stake period (#6)
This commit is contained in:
parent
bd0dc070ba
commit
c4d7a9f3c3
|
@ -1,43 +1,98 @@
|
||||||
pragma solidity ^0.5.0;
|
/* solium-disable security/no-block-members */
|
||||||
|
/* solium-disable security/no-inline-assembly */
|
||||||
|
pragma solidity >=0.5.0 <0.6.0;
|
||||||
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
|
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
|
||||||
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
|
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
|
||||||
import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol";
|
import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol";
|
||||||
import "openzeppelin-solidity/contracts/token/ERC20/ERC20Burnable.sol";
|
import "openzeppelin-solidity/contracts/token/ERC20/ERC20Burnable.sol";
|
||||||
import "./math.sol";
|
import "./math.sol";
|
||||||
import "./token/ApproveAndCallFallBack.sol";
|
import "./token/ApproveAndCallFallBack.sol";
|
||||||
|
import "./token/MiniMeTokenInterface.sol";
|
||||||
|
|
||||||
contract StakingPool is ERC20, ERC20Detailed, ERC20Burnable, DSMath, ApproveAndCallFallBack {
|
contract StakingPool is ERC20, ERC20Detailed, ERC20Burnable, DSMath, ApproveAndCallFallBack {
|
||||||
uint private INITIAL_SUPPLY = 0;
|
uint public MAX_SUPPLY = 0;
|
||||||
IERC20 public token;
|
|
||||||
|
|
||||||
constructor (address tokenAddress) public ERC20Detailed("TellerStatus", "TSNT", 18) {
|
MiniMeTokenInterface public token;
|
||||||
token = IERC20(tokenAddress);
|
uint public stakingBlockLimit;
|
||||||
|
uint public blockToCheckBalance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param _tokenAddress SNT token address
|
||||||
|
* @param _stakingPeriodLen Number of block that represents the period when Staking is available
|
||||||
|
*/
|
||||||
|
constructor (address _tokenAddress, uint _stakingPeriodLen) public ERC20Detailed("Status Stake Token", "SST", 18) {
|
||||||
|
token = MiniMeTokenInterface(_tokenAddress);
|
||||||
|
stakingBlockLimit = block.number + _stakingPeriodLen;
|
||||||
|
blockToCheckBalance = block.number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Determine exchange rate
|
||||||
|
* @return Exchange rate
|
||||||
|
*/
|
||||||
function exchangeRate (uint256 excludeAmount) public view returns (uint256) {
|
function exchangeRate (uint256 excludeAmount) public view returns (uint256) {
|
||||||
if (totalSupply() == 0) return 1000000000000000000;
|
if (totalSupply() == 0) return 1000000000000000000;
|
||||||
return wdiv(token.balanceOf(address(this)), totalSupply());
|
return wdiv(token.balanceOf(address(this)), totalSupply());
|
||||||
}
|
}
|
||||||
|
|
||||||
function estimatedTokens(uint256 value) public view returns (uint256) {
|
/**
|
||||||
uint256 rate = exchangeRate(value);
|
* @notice Estimate the number of tokens that will be minted based on an amount of SNT
|
||||||
return wdiv(value, wdiv(rate, 1000000000000000000));
|
* @param _value Amount of SNT used in calculation
|
||||||
|
*/
|
||||||
|
function estimatedTokens(uint256 _value) public view returns (uint256) {
|
||||||
|
uint256 rate = exchangeRate(_value);
|
||||||
|
return wdiv(_value, wdiv(rate, 1000000000000000000));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deposit (uint256 amount) public payable {
|
/**
|
||||||
_deposit(msg.sender, amount);
|
* @notice Determine max amount that can be staked
|
||||||
|
* @return Max amount to stake
|
||||||
|
*/
|
||||||
|
function maxAmountToStake() public view returns (uint256) {
|
||||||
|
return MAX_SUPPLY - totalSupply();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _deposit(address _from, uint256 amount) internal {
|
/**
|
||||||
uint256 equivalentTokens = estimatedTokens(amount);
|
* @notice Stake SNT in the pool and receive tSNT. During the stake period you can stake up to the amount of SNT you had when the pool was created. Afterwards, the amount you can stake can not exceed MAXSUPPLY - TOTALSUPPLY
|
||||||
require(token.transferFrom(_from, address(this), amount), "Couldn't transfer");
|
* @dev Use this function with approveAndCall, since it requires a SNT transfer
|
||||||
|
* @param _amount Amount to stake
|
||||||
|
*/
|
||||||
|
function stake(uint256 _amount) public payable {
|
||||||
|
if(block.number <= stakingBlockLimit){
|
||||||
|
uint maxBalance = token.balanceOfAt(msg.sender, blockToCheckBalance);
|
||||||
|
require(_amount <= maxBalance, "Stake amount exceeds SNT balance at pool creation");
|
||||||
|
_stake(msg.sender, _amount);
|
||||||
|
MAX_SUPPLY = totalSupply();
|
||||||
|
} else {
|
||||||
|
uint maxAmountToStake = MAX_SUPPLY - totalSupply();
|
||||||
|
require(_amount <= maxAmountToStake, "Max stake amount exceeded");
|
||||||
|
_stake(msg.sender, _amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Stake SNT in the contract, and receive tSNT
|
||||||
|
* @param _from Address transfering the SNT
|
||||||
|
* @param _amount Amount being staked
|
||||||
|
*/
|
||||||
|
function _stake(address _from, uint256 _amount) internal returns (uint equivalentTokens){
|
||||||
|
equivalentTokens = estimatedTokens(_amount);
|
||||||
|
require(token.transferFrom(_from, address(this), _amount), "Couldn't transfer");
|
||||||
_mint(_from, equivalentTokens);
|
_mint(_from, equivalentTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Withdraw SNT from Staking Pool, by burning tSNT
|
||||||
|
* @param amount Amount to withdraw
|
||||||
|
*/
|
||||||
function withdraw (uint256 amount) public {
|
function withdraw (uint256 amount) public {
|
||||||
uint256 rate = exchangeRate(0);
|
uint256 rate = exchangeRate(0);
|
||||||
burn(amount);
|
burn(amount);
|
||||||
|
|
||||||
|
if(block.number <= stakingBlockLimit){
|
||||||
|
MAX_SUPPLY = totalSupply();
|
||||||
|
}
|
||||||
|
|
||||||
require(token.transfer(msg.sender, wmul(amount, wdiv(rate, 1000000000000000000))), "Couldn't transfer");
|
require(token.transfer(msg.sender, wmul(amount, wdiv(rate, 1000000000000000000))), "Couldn't transfer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +101,7 @@ contract StakingPool is ERC20, ERC20Detailed, ERC20Burnable, DSMath, ApproveAndC
|
||||||
* @param _from Who approved.
|
* @param _from Who approved.
|
||||||
* @param _amount Amount being approved,
|
* @param _amount Amount being approved,
|
||||||
* @param _token Token being approved, need to be equal `token()`.
|
* @param _token Token being approved, need to be equal `token()`.
|
||||||
* @param _data Abi encoded data`.
|
* @param _data ABI encoded data`.
|
||||||
*/
|
*/
|
||||||
function receiveApproval(address _from, uint256 _amount, address _token, bytes memory _data) public {
|
function receiveApproval(address _from, uint256 _amount, address _token, bytes memory _data) public {
|
||||||
require(_token == address(token), "Wrong token");
|
require(_token == address(token), "Wrong token");
|
||||||
|
@ -55,14 +110,18 @@ contract StakingPool is ERC20, ERC20Detailed, ERC20Burnable, DSMath, ApproveAndC
|
||||||
|
|
||||||
bytes4 sig;
|
bytes4 sig;
|
||||||
uint amount;
|
uint amount;
|
||||||
(sig, amount) = abiDecodeRegister(_data);
|
(sig, amount) = abiDecode(_data);
|
||||||
|
|
||||||
require(amount == _amount, "Amounts mismatch");
|
require(amount == _amount, "Amounts mismatch");
|
||||||
require(sig == 0xb6b55f25, "Wrong method selector"); // deposit(uint256)
|
require(sig == 0xa694fc3a, "Wrong method selector"); // stake(uint256)
|
||||||
_deposit(_from, amount);
|
_stake(_from, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function abiDecodeRegister(bytes memory _data) private returns(
|
/**
|
||||||
|
* @dev Decode calldata - stake(uint256)
|
||||||
|
* @param _data Calldata, ABI encoded
|
||||||
|
*/
|
||||||
|
function abiDecode(bytes memory _data) internal returns(
|
||||||
bytes4 sig,
|
bytes4 sig,
|
||||||
uint256 amount
|
uint256 amount
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// /*global contract, config, it, assert, artifacts*/
|
// /*global contract, config, it, assert, artifacts*/
|
||||||
const StakingPool = artifacts.require('StakingPool');
|
let StakingPool = artifacts.require('StakingPool');
|
||||||
const SNT = artifacts.require('SNT');
|
const SNT = artifacts.require('SNT');
|
||||||
|
|
||||||
let iuri, jonathan, richard;
|
let iuri, jonathan, richard;
|
||||||
|
@ -24,6 +24,7 @@ config({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"StakingPool": {
|
"StakingPool": {
|
||||||
|
"deploy": false,
|
||||||
"args": ["$SNT"]
|
"args": ["$SNT"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +33,9 @@ config({
|
||||||
iuri = accounts[0];
|
iuri = accounts[0];
|
||||||
jonathan = accounts[1];
|
jonathan = accounts[1];
|
||||||
richard = accounts[2];
|
richard = accounts[2];
|
||||||
|
pascal = accounts[3];
|
||||||
|
michael = accounts[4];
|
||||||
|
eric = accounts[5];
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: add asserts for balances
|
// TODO: add asserts for balances
|
||||||
|
@ -45,6 +49,12 @@ contract("StakingPool", function () {
|
||||||
await SNT.methods.transfer(jonathan, "1000000000000000000000").send({from: iuri});
|
await SNT.methods.transfer(jonathan, "1000000000000000000000").send({from: iuri});
|
||||||
await SNT.methods.transfer(richard, "1000000000000000000000").send({from: iuri});
|
await SNT.methods.transfer(richard, "1000000000000000000000").send({from: iuri});
|
||||||
|
|
||||||
|
await SNT.methods.generateTokens(pascal, "1000000000000000000").send({from: iuri});
|
||||||
|
await SNT.methods.generateTokens(michael, "1000000000000000000").send({from: iuri});
|
||||||
|
|
||||||
|
// Deploy Staking Pool
|
||||||
|
StakingPool = await StakingPool.deploy({ arguments: [SNT.options.address, 100] }).send();
|
||||||
|
|
||||||
// approve StakingPool to transfer tokens
|
// approve StakingPool to transfer tokens
|
||||||
let balance;
|
let balance;
|
||||||
balance = await SNT.methods.balanceOf(iuri).call();
|
balance = await SNT.methods.balanceOf(iuri).call();
|
||||||
|
@ -53,11 +63,13 @@ contract("StakingPool", function () {
|
||||||
|
|
||||||
balance = await SNT.methods.balanceOf(jonathan).call();
|
balance = await SNT.methods.balanceOf(jonathan).call();
|
||||||
assert.strictEqual(balance, "1000000000000000000000");
|
assert.strictEqual(balance, "1000000000000000000000");
|
||||||
await SNT.methods.approve(StakingPool.address, "10000000000000000000000").send({from: jonathan});
|
await SNT.methods.approve(StakingPool.options.address, "10000000000000000000000").send({from: jonathan});
|
||||||
|
|
||||||
balance = await SNT.methods.balanceOf(richard).call();
|
balance = await SNT.methods.balanceOf(richard).call();
|
||||||
assert.strictEqual(balance, "1000000000000000000000");
|
assert.strictEqual(balance, "1000000000000000000000");
|
||||||
await SNT.methods.approve(StakingPool.address, "10000000000000000000000").send({from: richard});
|
await SNT.methods.approve(StakingPool.options.address, "10000000000000000000000").send({from: richard});
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("initial state", () => {
|
describe("initial state", () => {
|
||||||
|
@ -72,7 +84,7 @@ contract("StakingPool", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("initial balance should be 0", async function () {
|
it("initial balance should be 0", async function () {
|
||||||
let balance = await SNT.methods.balanceOf(StakingPool.address).call();
|
let balance = await SNT.methods.balanceOf(StakingPool.options.address).call();
|
||||||
assert.strictEqual(balance, "0");
|
assert.strictEqual(balance, "0");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -80,7 +92,7 @@ contract("StakingPool", function () {
|
||||||
describe("depositing before contributions", () => {
|
describe("depositing before contributions", () => {
|
||||||
before("deposit 11 ETH", async () => {
|
before("deposit 11 ETH", async () => {
|
||||||
// Deposit using approveAndCall
|
// Deposit using approveAndCall
|
||||||
const encodedCall = StakingPool.methods.deposit("11000000000000000000").encodeABI();
|
const encodedCall = StakingPool.methods.stake("11000000000000000000").encodeABI();
|
||||||
await SNT.methods.approveAndCall(StakingPool.options.address, "11000000000000000000", encodedCall).send({from: iuri});
|
await SNT.methods.approveAndCall(StakingPool.options.address, "11000000000000000000", encodedCall).send({from: iuri});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -95,14 +107,14 @@ contract("StakingPool", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("balance should be 12", async function () {
|
it("balance should be 12", async function () {
|
||||||
let balance = await SNT.methods.balanceOf(StakingPool.address).call();
|
let balance = await SNT.methods.balanceOf(StakingPool.options.address).call();
|
||||||
assert.strictEqual(balance, "11000000000000000000");
|
assert.strictEqual(balance, "11000000000000000000");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("2nd person depositing before contributions", () => {
|
describe("2nd person depositing before contributions", () => {
|
||||||
before("deposit 5 ETH", async () => {
|
before("deposit 5 ETH", async () => {
|
||||||
await StakingPool.methods.deposit("5000000000000000000").send({from: jonathan})
|
await StakingPool.methods.stake("5000000000000000000").send({from: jonathan})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("exchangeRate should remain 1", async function () {
|
it("exchangeRate should remain 1", async function () {
|
||||||
|
@ -116,14 +128,14 @@ contract("StakingPool", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("balance should be 17", async function () {
|
it("balance should be 17", async function () {
|
||||||
let balance = await SNT.methods.balanceOf(StakingPool.address).call();
|
let balance = await SNT.methods.balanceOf(StakingPool.options.address).call();
|
||||||
assert.strictEqual(balance, "16000000000000000000");
|
assert.strictEqual(balance, "16000000000000000000");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("contributions", () => {
|
describe("contributions", () => {
|
||||||
before("contribute 10 ETH", async () => {
|
before("contribute 10 ETH", async () => {
|
||||||
await SNT.methods.transfer(StakingPool.address, "10000000000000000000").send({from: iuri});
|
await SNT.methods.transfer(StakingPool.options.address, "10000000000000000000").send({from: iuri});
|
||||||
})
|
})
|
||||||
|
|
||||||
it("exchangeRate should increase", async function () {
|
it("exchangeRate should increase", async function () {
|
||||||
|
@ -137,7 +149,7 @@ contract("StakingPool", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("balance should be 27", async function () {
|
it("balance should be 27", async function () {
|
||||||
let balance = await SNT.methods.balanceOf(StakingPool.address).call();
|
let balance = await SNT.methods.balanceOf(StakingPool.options.address).call();
|
||||||
assert.strictEqual(balance, "26000000000000000000");
|
assert.strictEqual(balance, "26000000000000000000");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -158,7 +170,7 @@ contract("StakingPool", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("balance should decrease by correct exchange rate", async function () {
|
it("balance should decrease by correct exchange rate", async function () {
|
||||||
let balance = await SNT.methods.balanceOf(StakingPool.address).call();
|
let balance = await SNT.methods.balanceOf(StakingPool.options.address).call();
|
||||||
// 5000000000000000000 tokens x 1.625 rate
|
// 5000000000000000000 tokens x 1.625 rate
|
||||||
// => 8125000000000000000 ETH
|
// => 8125000000000000000 ETH
|
||||||
// 26000000000000000000 - 8125000000000000000 = 17875000000000000000
|
// 26000000000000000000 - 8125000000000000000 = 17875000000000000000
|
||||||
|
@ -168,7 +180,7 @@ contract("StakingPool", function () {
|
||||||
|
|
||||||
describe("depositing after contributions", () => {
|
describe("depositing after contributions", () => {
|
||||||
before("deposit 8 ETH", async () => {
|
before("deposit 8 ETH", async () => {
|
||||||
await StakingPool.methods.deposit("8000000000000000000").send({from: richard})
|
await StakingPool.methods.stake("8000000000000000000").send({from: richard})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("exchangeRate should remain the same", async function () {
|
it("exchangeRate should remain the same", async function () {
|
||||||
|
@ -182,10 +194,40 @@ contract("StakingPool", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("balance should increase", async function () {
|
it("balance should increase", async function () {
|
||||||
let balance = await SNT.methods.balanceOf(StakingPool.address).call();
|
let balance = await SNT.methods.balanceOf(StakingPool.options.address).call();
|
||||||
// 17875000000000000000 + 8000000000000000000
|
// 17875000000000000000 + 8000000000000000000
|
||||||
assert.strictEqual(balance, "25875000000000000000");
|
assert.strictEqual(balance, "25875000000000000000");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Checking stake conditions", () => {
|
||||||
|
|
||||||
|
it("should not allow stakeing more than the balance had at the moment of staking pool deployment", async () => {
|
||||||
|
await SNT.methods.transfer(pascal, "1000000000000000000").send({from: iuri}); // Pascal now has 2 eth
|
||||||
|
await SNT.methods.approve(StakingPool.options.address, "2000000000000000000").send({from: pascal});
|
||||||
|
await assert.reverts(StakingPool.methods.stake("2000000000000000000"), {from: pascal}, "Returned error: VM Exception while processing transaction: revert Stake amount exceeds SNT balance at pool creation");
|
||||||
|
await StakingPool.methods.stake("1000000000000000000").send({from: pascal});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow stake only after a pool user withdraws", async () => {
|
||||||
|
// Mine 100 blocks
|
||||||
|
for(let i = 0; i < 100; i++){
|
||||||
|
await mineAtTimestamp(12345678);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SNT.methods.approve(StakingPool.options.address, "1000000000000000000").send({from: michael});
|
||||||
|
await assert.reverts(StakingPool.methods.stake("1000000000000000000"), {from: michael}, "Returned error: VM Exception while processing transaction: revert Max stake amount exceeded");
|
||||||
|
|
||||||
|
assert.strictEqual(await StakingPool.methods.maxAmountToStake().call(), "0");
|
||||||
|
|
||||||
|
await StakingPool.methods.withdraw("1000000000000000000").send({from: richard})
|
||||||
|
|
||||||
|
assert.strictEqual(await StakingPool.methods.maxAmountToStake().call(), "1000000000000000000");
|
||||||
|
|
||||||
|
await SNT.methods.transfer(eric, "1000000000000000000").send({from: michael});
|
||||||
|
await SNT.methods.approve(StakingPool.options.address, "1000000000000000000").send({from: eric});
|
||||||
|
await StakingPool.methods.stake("1000000000000000000").send({from: eric});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue