diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index f773fcf..a74010e 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "hardhat/console.sol"; + interface IRegistry { function getExchange(address _tokenAddress) external returns (address); diff --git a/contracts/StakingRewards.sol b/contracts/StakingRewards.sol new file mode 100644 index 0000000..5968dc8 --- /dev/null +++ b/contracts/StakingRewards.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +import "hardhat/console.sol"; + +contract StakingRewards { + IERC20 public rewardsToken; + IERC20 public stakingToken; + + uint public rewardRate = 100; + uint public lastUpdateTime; + uint public rewardPerTokenStored; + + mapping(address => uint) public userRewardPerTokenPaid; + mapping(address => uint) public rewards; + + uint private _totalSupply; + mapping(address => uint) private _balances; + + /* ========== CONSTRUCTOR ========== */ + constructor(address _stakingToken, address _rewardsToken) { + stakingToken = IERC20(_stakingToken); + rewardsToken = IERC20(_rewardsToken); + } + + /* ========== VIEWS ========== */ + function totalSupply() external view returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) external view returns (uint256) { + return _balances[account]; + } + + function rewardPerToken() public view returns (uint) { + //console.log("RewardsPerToken total supply: %s rewardPerTokenStored %s",_totalSupply,rewardPerTokenStored); + if (_totalSupply == 0) { + return rewardPerTokenStored; + } + return + rewardPerTokenStored + + (((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / _totalSupply); + } + + function earned(address account) public view returns (uint) { + return + ((_balances[account] * + (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18) + + rewards[account]; + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + function stake(uint _amount) external updateReward(msg.sender) { + require(_amount > 0, "Cannot stake 0"); + _totalSupply += _amount; + _balances[msg.sender] += _amount; + stakingToken.transferFrom(msg.sender, address(this), _amount); + + //console.log("Stake %s tokens", _amount); + } + + function withdraw(uint _amount) public updateReward(msg.sender) { + require(_amount > 0, "Cannot withdraw 0"); + _totalSupply -= _amount; + _balances[msg.sender] -= _amount; + stakingToken.transfer(msg.sender, _amount); + + //console.log("withdraw %s tokens \ntotal supply %s", _amount,_totalSupply); + + } + + function getReward() public updateReward(msg.sender) { + //console.log("Rewards on get", rewards[msg.sender],rewardPerTokenStored); + uint reward = rewards[msg.sender]; + if (reward > 0) { + rewards[msg.sender] = 0; + rewardsToken.transferFrom(address(this), msg.sender, reward); + } + + } + + function exit() external { + withdraw(_balances[msg.sender]); + getReward(); + } + + /// @notice add the tokens to be distributed as rewards + /// @param _amount token amount (token address is the one on constructor) + function depositRewardsTokens(uint256 _amount) external { + rewardsToken.transferFrom(msg.sender, address(this), _amount); + rewardsToken.approve(address(this), _amount); + } + + /* ========== MODIFIERS ========== */ + modifier updateReward(address account) { + rewardPerTokenStored = rewardPerToken(); + lastUpdateTime = block.timestamp; + + rewards[account] = earned(account); + userRewardPerTokenPaid[account] = rewardPerTokenStored; + _; + } + +} + +interface IERC20 { + function totalSupply() external view returns (uint); + + function balanceOf(address account) external view returns (uint); + + function transfer(address recipient, uint amount) external returns (bool); + + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint amount) external returns (bool); + + function transferFrom( + address sender, + address recipient, + uint amount + ) external returns (bool); + + event Transfer(address indexed from, address indexed to, uint value); + event Approval(address indexed owner, address indexed spender, uint value); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eece4b1..7f0c475 100644 --- a/package-lock.json +++ b/package-lock.json @@ -768,6 +768,7 @@ } }, "@nomiclabs/hardhat-ethers": { + "version": "2.0.3", "resolved": "https://registry.npmjs.org/@nomiclabs/hardhat-ethers/-/hardhat-ethers-2.0.3.tgz", "integrity": "sha512-IJ0gBotVtO7YyLZyHNgbxzskUtFok+JkRlKPo8YELqj1ms9XL6Qm3vsfsGdZr22wnJeVEF5TQPotKuwQk21Dag==", @@ -7673,7 +7674,8 @@ }, "keccak": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", "dev": true, "requires": { "node-addon-api": "^2.0.0", @@ -8220,7 +8222,8 @@ }, "node-addon-api": { "version": "2.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", "dev": true }, "node-fetch": { @@ -8231,7 +8234,8 @@ }, "node-gyp-build": { "version": "4.2.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "dev": true }, "normalize-url": { diff --git a/test/exchange-test.js b/test/exchange-test.js index 83a358a..bd3c068 100644 --- a/test/exchange-test.js +++ b/test/exchange-test.js @@ -5,12 +5,16 @@ const { provider } = waffle; const totalSupply = ethers.utils.parseEther("10000"); const amountA = ethers.utils.parseEther("2000"); const amountB = ethers.utils.parseEther("1000"); + let token; let exchange; +let rewardsToken; +let rewards; + +let deployer, bob, alice; let tx; -let deployer, bob, alice; describe("Exchange", function () { beforeEach(async function () { @@ -21,6 +25,20 @@ describe("Exchange", function () { const Exchange = await ethers.getContractFactory("Exchange"); exchange = await Exchange.deploy(token.address) + + // Deploy otro ERC-20 para dar rewards + rewardsToken = await Token.deploy("FreeMoney", "F$$", totalSupply); + await rewardsToken.deployed(); + + // Deploy el contrato de rewards + const Rewards = await ethers.getContractFactory("StakingRewards"); + rewards = await Rewards.deploy(exchange.address, rewardsToken.address) + + // Manda todos los rewardsToken al contrato de rewards + const deployerRewardsTokens = await rewardsToken.balanceOf(deployer.address) + await rewardsToken.approve(rewards.address, deployerRewardsTokens) + await rewards.depositRewardsTokens(deployerRewardsTokens) + }); it("add liquidity", async function () { @@ -29,10 +47,55 @@ describe("Exchange", function () { await expect(tx).to.emit(exchange, "AddLiquidity") .withArgs(deployer.address, amountB, amountA); + expect(await exchange.balanceOf(deployer.address)).to.equal(ethers.utils.parseUnits("1000")); expect(await provider.getBalance(exchange.address)).to.equal(amountB); expect(await exchange.getReserve()).to.equal(amountA); }); + it("Verificar StakingRewards.sol (rewards) fue deployed correctamente", async function () { + + expect(await rewards.stakingToken()).to.equal(exchange.address); + expect(await rewards.rewardsToken()).to.equal(rewardsToken.address); + expect(await rewardsToken.balanceOf(deployer.address)).to.equal(0); + expect(await rewardsToken.balanceOf(rewards.address)).to.equal(ethers.utils.parseUnits("10000")); + + }); + + it("Stake exchange tokens (LP) y recibir rewards", async () => { + await token.approve(exchange.address, amountA); + await exchange.addLiquidity(amountA, { value: amountB }); + + let tokens_to_be_staked = await exchange.balanceOf(deployer.address) + await exchange.approve(rewards.address, tokens_to_be_staked); + await rewards.stake(tokens_to_be_staked) + + expect(await exchange.balanceOf(rewards.address)).to.equal(tokens_to_be_staked); + expect(parseInt(await rewards.rewardPerToken())).to.equal(0); + + // simular 20 bloques + let numberOfBlocks = 20 + for (let i = 0; i < numberOfBlocks; i++) { + await ethers.provider.send("evm_increaseTime", [60]); // 60 segunods + await ethers.provider.send("evm_mine", []); // add 60 secs + } + + // ver staking rewards + console.log(` STAKING REWARDS (${numberOfBlocks} bloques):\n > rewardsPerToken: ${parseInt(await rewards.rewardPerToken())}\n > earned: ${parseInt(await rewards.earned(deployer.address))}`) + + + expect(parseInt(await rewards.rewardPerToken())).to.equal(120); + expect(parseInt(await rewards.earned(deployer.address))).equal(120000); + + await rewards.withdraw(tokens_to_be_staked) + expect(await exchange.balanceOf(rewards.address)).to.equal(0); + + await rewards.getReward() + expect(await rewardsToken.balanceOf(deployer.address)).to.equal(120000); + + // by Kayaba_Attribution + + }); + it("returns correct token price", async () => { // await token.approve(exchange.address, amountA); // await exchange.addLiquidity(amountA, { value: amountB }); @@ -87,6 +150,7 @@ describe("Exchange", function () { bar = await exchange.getEthAmount(ethers.utils.parseEther("2000")); expect(ethers.utils.formatEther(bar)).to.eq("497.487437185929648241"); + }); it("swap eth into token", async () => { @@ -109,5 +173,6 @@ describe("Exchange", function () { await expect(tx).to.emit(exchange, "TokenPurchase"); // // revisar estos valores // expect(await token.balanceOf(alice.address)).to.eq(expectedOutputForAlice); + }); });