From 8b816a9ad66a579006c14f4edaab62ab5217c2f9 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Thu, 30 May 2024 13:30:13 -0700 Subject: [PATCH 01/13] rewards reciever test finished --- test/v2/core/rewardsReceiver.spec.js | 104 +++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/v2/core/rewardsReceiver.spec.js diff --git a/test/v2/core/rewardsReceiver.spec.js b/test/v2/core/rewardsReceiver.spec.js new file mode 100644 index 0000000..a74eb64 --- /dev/null +++ b/test/v2/core/rewardsReceiver.spec.js @@ -0,0 +1,104 @@ +const {ethers} = require("hardhat"); +const {expect} = require("chai"); +const {parseEther} = require("ethers/lib/utils"); + +describe("RewardsReceiver", () => { + let rewardsReceiver, sgEth, paymentSplitter, minter, withdrawals, wsgEth, deployer, alice, multiSig; + + beforeEach(async () => { + const [owner, addr1, addr2] = await ethers.getSigners(); + + const SgETH = await ethers.getContractFactory("SgETH"); + sgEth = await SgETH.deploy([]); + await sgEth.deployed(); + + deployer = owner; + alice = addr1; + multiSig = addr2; + + MINTER_ROLE = await sgEth.MINTER(); + + // deploy sgeth + const WSGETH = await ethers.getContractFactory("WSGETH"); + wsgEth = await WSGETH.deploy(sgEth.address, 24 * 60 * 60); + await wsgEth.deployed(); + + const splitterAddresses = [deployer.address, multiSig.address, wsgEth.address]; + const splitterValues = [6, 3, 31]; + + const PaymentSplitter = await ethers.getContractFactory("PaymentSplitter"); + paymentSplitter = await PaymentSplitter.deploy(splitterAddresses, splitterValues); + await paymentSplitter.deployed(); + + const rolloverVirtual = "1080000000000000000"; + const vETH2Addr = "0x898bad2774eb97cf6b94605677f43b41871410b1"; + + const Withdrawals = await ethers.getContractFactory("Withdrawals"); + withdrawals = await Withdrawals.deploy(vETH2Addr, rolloverVirtual); + await withdrawals.deployed(); + + const numValidators = 1000; + const adminFee = 0; + + const FeeCalc = await ethers.getContractFactory("FeeCalc"); + const feeCalc = await FeeCalc.deploy(parseEther("0.1"), parseEther("0.1")); + await feeCalc.deployed(); + + const addresses = [ + feeCalc.address, // fee splitter + sgEth.address, // sgETH address + wsgEth.address, // wsgETH address + multiSig.address, // government address + ethers.constants.AddressZero, // deposit contract address - can't find deposit contract - using dummy address + ]; + + // add secondary minter contract / eoa + const Minter = await ethers.getContractFactory("SharedDepositMinterV2"); + minter = await Minter.deploy(numValidators, adminFee, addresses); + await minter.deployed(); + + const RewardsReceiver = await ethers.getContractFactory("RewardsReceiver"); + rewardsReceiver = await RewardsReceiver.deploy(withdrawals.address, [ + sgEth.address, + wsgEth.address, + paymentSplitter.address, + minter.address, + ]); + await rewardsReceiver.deployed(); + + await sgEth.addMinter(minter.address); + }); + + it("work", async () => { + // deposit eth for test + await deployer.sendTransaction({ + to: rewardsReceiver.address, + value: parseEther("1"), + }); + + let prevBalance = await deployer.provider.getBalance(rewardsReceiver.address); + console.log(prevBalance); + await rewardsReceiver.work(); + let afterBalance = await deployer.provider.getBalance(rewardsReceiver.address); + expect(afterBalance).to.eq(prevBalance.sub(parseEther("1"))); + + await deployer.sendTransaction({ + to: rewardsReceiver.address, + value: parseEther("1"), + }); + await rewardsReceiver.flipState(); + + prevBalance = await deployer.provider.getBalance(rewardsReceiver.address); + await rewardsReceiver.work(); + afterBalance = await deployer.provider.getBalance(rewardsReceiver.address); + expect(afterBalance).to.eq(prevBalance.sub(parseEther("1"))); + }); + + it("flipState", async () => { + await expect(rewardsReceiver.connect(alice).flipState()).to.be.revertedWith("Ownable: caller is not the owner"); + + expect(await rewardsReceiver.state()).to.eq(0); + await rewardsReceiver.flipState(); + expect(await rewardsReceiver.state()).to.eq(1); + }); +}); From c6bbd7e2806b03588ba052baa7d4b53744b708d9 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Fri, 31 May 2024 07:37:24 -0700 Subject: [PATCH 02/13] SharedDepositMinterV2 test finished --- test/v2/core/minter.spec.js | 211 ++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 test/v2/core/minter.spec.js diff --git a/test/v2/core/minter.spec.js b/test/v2/core/minter.spec.js new file mode 100644 index 0000000..a3fdf14 --- /dev/null +++ b/test/v2/core/minter.spec.js @@ -0,0 +1,211 @@ +const {ethers} = require("hardhat"); +const {expect} = require("chai"); +const {parseEther} = require("ethers/lib/utils"); + +describe.only("SharedDepositMinterV2", () => { + let sgEth, paymentSplitter, minter, withdrawals, wsgEth, deployer, alice, multiSig; + + beforeEach(async () => { + const [owner, addr1, addr2] = await ethers.getSigners(); + + const SgETH = await ethers.getContractFactory("SgETH"); + sgEth = await SgETH.deploy([]); + await sgEth.deployed(); + + deployer = owner; + alice = addr1; + multiSig = addr2; + + MINTER_ROLE = await sgEth.MINTER(); + + // deploy sgeth + const WSGETH = await ethers.getContractFactory("WSGETH"); + wsgEth = await WSGETH.deploy(sgEth.address, 24 * 60 * 60); + await wsgEth.deployed(); + + const splitterAddresses = [deployer.address, multiSig.address, wsgEth.address]; + const splitterValues = [6, 3, 31]; + + const PaymentSplitter = await ethers.getContractFactory("PaymentSplitter"); + paymentSplitter = await PaymentSplitter.deploy(splitterAddresses, splitterValues); + await paymentSplitter.deployed(); + + const rolloverVirtual = "1080000000000000000"; + const vETH2Addr = "0x898bad2774eb97cf6b94605677f43b41871410b1"; + + const Withdrawals = await ethers.getContractFactory("Withdrawals"); + withdrawals = await Withdrawals.deploy(vETH2Addr, rolloverVirtual); + await withdrawals.deployed(); + + const numValidators = 1000; + const adminFee = 0; + + const FeeCalc = await ethers.getContractFactory("FeeCalc"); + const feeCalc = await FeeCalc.deploy(parseEther("0"), parseEther("0")); + await feeCalc.deployed(); + + const addresses = [ + feeCalc.address, // fee splitter + sgEth.address, // sgETH address + wsgEth.address, // wsgETH address + multiSig.address, // government address + ethers.constants.AddressZero, // deposit contract address - can't find deposit contract - using dummy address + ]; + + // add secondary minter contract / eoa + const Minter = await ethers.getContractFactory("SharedDepositMinterV2"); + minter = await Minter.deploy(numValidators, adminFee, addresses); + await minter.deployed(); + + // const RewardsReceiver = await ethers.getContractFactory("RewardsReceiver"); + // rewardsReceiver = await RewardsReceiver.deploy(withdrawals.address, [ + // sgEth.address, + // wsgEth.address, + // paymentSplitter.address, + // minter.address, + // ]); + // await rewardsReceiver.deployed(); + + await sgEth.addMinter(minter.address); + }); + + it("deposit", async () => { + const prevBalance = await sgEth.balanceOf(alice.address); + await minter.connect(alice).deposit({ + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(alice.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); + }); + + it("depositFor", async () => { + const prevBalance = await sgEth.balanceOf(alice.address); + await minter.depositFor(alice.address, { + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(alice.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); + }); + + it("depositAndStake", async () => { + const prevStake = await wsgEth.maxRedeem(deployer.address); + const prevBalance = await sgEth.balanceOf(wsgEth.address); + await minter.depositAndStake({ + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(wsgEth.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); + + const afterStake = await wsgEth.maxRedeem(deployer.address); + expect(afterStake).to.eq(prevStake.add(parseEther("1"))); + }); + + it("depositAndStakeFor", async () => { + const prevStake = await wsgEth.maxRedeem(alice.address); + const prevBalance = await sgEth.balanceOf(wsgEth.address); + await minter.depositAndStakeFor(alice.address, { + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(wsgEth.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); + + const afterStake = await wsgEth.maxRedeem(alice.address); + expect(afterStake).to.eq(prevStake.add(parseEther("1"))); + }); + + it("withdraw, withdrawTo", async () => { + await minter.connect(alice).deposit({ + value: parseEther("1"), + }); + await expect(minter.connect(alice).withdraw(parseEther("1.1"))).to.be.revertedWith(""); + + let prevBalance = await sgEth.balanceOf(alice.address); + await minter.connect(alice).withdraw(parseEther("0.5")); + let afterBalance = await sgEth.balanceOf(alice.address); + + expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); + + prevBalance = await sgEth.balanceOf(alice.address); + await minter.connect(alice).withdrawTo(parseEther("0.5"), alice.address); + afterBalance = await sgEth.balanceOf(alice.address); + + expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); + }); + + it("unstakeAndWithdraw", async () => { + await minter.connect(alice).depositAndStake({ + value: parseEther("1"), + }); + await expect(minter.connect(alice).unstakeAndWithdraw(parseEther("1.1"), alice.address)).to.be.revertedWith(""); + await expect(minter.connect(alice).unstakeAndWithdraw(parseEther("0.5"), alice.address)).to.be.revertedWith(""); + + await wsgEth.connect(alice).approve(minter.address, ethers.constants.MaxUint256); + let prevBalance = await wsgEth.balanceOf(alice.address); + await minter.connect(alice).unstakeAndWithdraw(parseEther("0.5"), alice.address); + let afterBalance = await wsgEth.balanceOf(alice.address); + + expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); + }); + + it("setWithdrawalCredential", async () => { + const NOR_ROLE = await minter.NOR(); + await expect(minter.connect(alice).setWithdrawalCredential("0x")).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${NOR_ROLE}`, + ); + + await minter.setWithdrawalCredential("0x"); + }); + + it("slash", async () => { + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).slash(parseEther("0.1"))).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + + await expect(minter.connect(multiSig).slash(parseEther("0.1"))).to.be.revertedWith("AmountTooHigh()"); + + await minter.connect(alice).depositAndStake({ + value: parseEther("10"), + }); + await expect(minter.connect(multiSig).slash(parseEther("0.1"))) + .to.be.emit(sgEth, "Transfer") + .withArgs(wsgEth.address, ethers.constants.AddressZero, parseEther("0.1")); + }); + + it("togglePause", async () => { + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).togglePause()).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + + await expect(minter.connect(multiSig).togglePause()).to.be.emit(minter, "Paused").withArgs(multiSig.address); + }); + + it("migrateShares", async () => { + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).migrateShares(parseEther("0.1"))).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + + await minter.connect(multiSig).migrateShares(parseEther("0.1")); + }); + + it("toggleWithdrawRefund", async () => { + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).toggleWithdrawRefund()).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + + await minter.connect(multiSig).toggleWithdrawRefund(); + }); + + it("setNumValidators", async () => { + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).setNumValidators(1)).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + await expect(minter.connect(multiSig).setNumValidators(0)).to.be.revertedWith("Minimum 1 validator"); + + await minter.connect(multiSig).setNumValidators(1); + }); +}); From ca8b60fe9d545b99648ecb5ecbe142228799d284 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Fri, 31 May 2024 07:46:59 -0700 Subject: [PATCH 03/13] removed test case --- test/v2/core/minter.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/v2/core/minter.spec.js b/test/v2/core/minter.spec.js index a3fdf14..3865b5f 100644 --- a/test/v2/core/minter.spec.js +++ b/test/v2/core/minter.spec.js @@ -2,7 +2,7 @@ const {ethers} = require("hardhat"); const {expect} = require("chai"); const {parseEther} = require("ethers/lib/utils"); -describe.only("SharedDepositMinterV2", () => { +describe("SharedDepositMinterV2", () => { let sgEth, paymentSplitter, minter, withdrawals, wsgEth, deployer, alice, multiSig; beforeEach(async () => { From bc514dcc2ae9a27b0ea9f9ebce0e65248c3172c7 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Sun, 2 Jun 2024 17:09:34 -0700 Subject: [PATCH 04/13] wsg eth test finished --- test/v2/core/wsgETH.spec.js | 122 ++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 test/v2/core/wsgETH.spec.js diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js new file mode 100644 index 0000000..54231eb --- /dev/null +++ b/test/v2/core/wsgETH.spec.js @@ -0,0 +1,122 @@ +const {ethers} = require("hardhat"); +const {expect} = require("chai"); +const {parseEther} = require("ethers/lib/utils"); + +describe.only("WsgETH.sol", () => { + let sgEth, wsgEth, deployer, alice; + let MINTER_ROLE; + + beforeEach(async () => { + const [owner, addr1] = await ethers.getSigners(); + + const SgETH = await ethers.getContractFactory("SgETH"); + sgEth = await SgETH.deploy([]); + await sgEth.deployed(); + + deployer = owner; + alice = addr1; + + MINTER_ROLE = await sgEth.MINTER(); + + // deploy wsgeth + const WSGETH = await ethers.getContractFactory("WSGETH"); + wsgEth = await WSGETH.deploy(sgEth.address, 24 * 60 * 60); + await wsgEth.deployed(); + + // mint tokens for test + await sgEth.addMinter(deployer.address); + await sgEth.mint(deployer.address, parseEther("1000")); + }); + + it("deposit", async () => { + await expect(wsgEth.connect(alice).deposit(parseEther("1"), alice.address)).to.be.revertedWith( + "TRANSFER_FROM_FAILED", + ); + await expect(wsgEth.deposit(parseEther("1"), alice.address)).to.be.revertedWith("TRANSFER_FROM_FAILED"); + + await sgEth.approve(wsgEth.address, parseEther("1")); + await expect(wsgEth.deposit(parseEther("1"), alice.address)) + .to.be.emit(wsgEth, "Transfer") + .withArgs(ethers.constants.AddressZero, alice.address, parseEther("1")); + }); + + it("mint", async () => { + await expect(wsgEth.connect(alice).mint(parseEther("1"), alice.address)).to.be.revertedWith("TRANSFER_FROM_FAILED"); + await expect(wsgEth.mint(parseEther("1"), alice.address)).to.be.revertedWith("TRANSFER_FROM_FAILED"); + + await sgEth.approve(wsgEth.address, parseEther("1")); + await expect(wsgEth.mint(parseEther("1"), alice.address)) + .to.be.emit(wsgEth, "Transfer") + .withArgs(ethers.constants.AddressZero, alice.address, parseEther("1")); + }); + + it("withdraw", async () => { + await expect(wsgEth.withdraw(parseEther("1"), alice.address, alice.address)).to.be.revertedWith(""); // panic revert by insufficient allowance + await wsgEth.connect(alice).approve(deployer.address, parseEther("1000")); + + await expect(wsgEth.withdraw(parseEther("1"), alice.address, alice.address)).to.be.revertedWith(""); // panic revert by insufficient balance + + // mint wsgEth to alice + await sgEth.approve(wsgEth.address, parseEther("1")); + await wsgEth.deposit(parseEther("1"), alice.address); + + await expect(wsgEth.withdraw(parseEther("1"), alice.address, alice.address)) + .to.be.emit(wsgEth, "Withdraw") + .withArgs(deployer.address, alice.address, alice.address, parseEther("1"), parseEther("1")); + }); + + it("redeem", async () => { + await expect(wsgEth.redeem(parseEther("1"), alice.address, alice.address)).to.be.revertedWith(""); // panic revert by insufficient allowance + await wsgEth.connect(alice).approve(deployer.address, parseEther("1000")); + + await expect(wsgEth.redeem(parseEther("1"), alice.address, alice.address)).to.be.revertedWith(""); // panic revert by insufficient balance + + // mint wsgEth to alice + await sgEth.approve(wsgEth.address, parseEther("1")); + await wsgEth.deposit(parseEther("1"), alice.address); + + await expect(wsgEth.redeem(parseEther("1"), alice.address, alice.address)) + .to.be.emit(wsgEth, "Withdraw") + .withArgs(deployer.address, alice.address, alice.address, parseEther("1"), parseEther("1")); + }); + + it("depositWithSignature", async () => { + await sgEth.transfer(alice.address, parseEther("1")); + const nonce = await sgEth.nonces(alice.address); + const deadline = Math.floor(Date.now() / 1000) + 1000; + const approveData = { + owner: alice.address, + spender: wsgEth.address, + value: parseEther("1"), + nonce, + deadline: deadline, + }; + + const domain = await sgEth.eip712Domain(); + + const signature = await alice._signTypedData( + { + name: domain.name, + version: domain.version, + chainId: domain.chainId, + verifyingContract: domain.verifyingContract, + }, + { + Permit: [ + {name: "owner", type: "address"}, + {name: "spender", type: "address"}, + {name: "value", type: "uint256"}, + {name: "nonce", type: "uint256"}, + {name: "deadline", type: "uint256"}, + ], + }, + approveData, + ); + + const {r, s, v} = ethers.utils.splitSignature(signature); + + // await expect(wsgEth.connect(alice).depositWithSignature(parseEther("1"), alice.address, deadline, false, v, r, s)) + // .to.be.emit(wsgEth, "Transfer") + // .withArgs(ethers.constants.AddressZero, alice.address, parseEther("1")); + }); +}); From 293aa63885ca20206df7bab2986f45397d8bb72c Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Wed, 5 Jun 2024 19:19:33 -0700 Subject: [PATCH 05/13] reentrant bugfix on wsgEth contract --- contracts/v2/core/WSGEth.sol | 2 +- test/v2/core/wsgETH.spec.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/v2/core/WSGEth.sol b/contracts/v2/core/WSGEth.sol index d9b4c73..d26e164 100644 --- a/contracts/v2/core/WSGEth.sol +++ b/contracts/v2/core/WSGEth.sol @@ -38,7 +38,7 @@ contract WSGETH is xERC4626, ReentrancyGuard { uint8 v, bytes32 r, bytes32 s - ) external nonReentrant returns (uint256 shares) { + ) external returns (uint256 shares) { uint256 amount = approveMax ? type(uint256).max : assets; asset.permit(msg.sender, address(this), amount, deadline, v, r, s); return (deposit(assets, receiver)); diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js index 54231eb..faa7e95 100644 --- a/test/v2/core/wsgETH.spec.js +++ b/test/v2/core/wsgETH.spec.js @@ -115,8 +115,8 @@ describe.only("WsgETH.sol", () => { const {r, s, v} = ethers.utils.splitSignature(signature); - // await expect(wsgEth.connect(alice).depositWithSignature(parseEther("1"), alice.address, deadline, false, v, r, s)) - // .to.be.emit(wsgEth, "Transfer") - // .withArgs(ethers.constants.AddressZero, alice.address, parseEther("1")); + await expect(wsgEth.connect(alice).depositWithSignature(parseEther("1"), alice.address, deadline, false, v, r, s)) + .to.be.emit(wsgEth, "Transfer") + .withArgs(ethers.constants.AddressZero, alice.address, parseEther("1")); }); }); From d620b9e006d90d28e13d58c3661a8df0d76dfb3f Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Wed, 5 Jun 2024 20:46:25 -0700 Subject: [PATCH 06/13] use zero address for feecalc contract --- test/v2/core/e2e.spec.js | 2 +- test/v2/core/minter.spec.js | 9 +++++---- test/v2/core/rewardsReceiver.spec.js | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/v2/core/e2e.spec.js b/test/v2/core/e2e.spec.js index 441f904..9da6d98 100644 --- a/test/v2/core/e2e.spec.js +++ b/test/v2/core/e2e.spec.js @@ -42,7 +42,7 @@ describe("e2e test", () => { const adminFee = 0; const addresses = [ - paymentSplitter.address, // fee splitter + ethers.constants.AddressZero, // fee splitter sgEth.address, // sgETH address wsgETH.address, // wsgETH address multiSig.address, // government address diff --git a/test/v2/core/minter.spec.js b/test/v2/core/minter.spec.js index 3865b5f..776c330 100644 --- a/test/v2/core/minter.spec.js +++ b/test/v2/core/minter.spec.js @@ -40,12 +40,13 @@ describe("SharedDepositMinterV2", () => { const numValidators = 1000; const adminFee = 0; - const FeeCalc = await ethers.getContractFactory("FeeCalc"); - const feeCalc = await FeeCalc.deploy(parseEther("0"), parseEther("0")); - await feeCalc.deployed(); + // const FeeCalc = await ethers.getContractFactory("FeeCalc"); + // const feeCalc = await FeeCalc.deploy(parseEther("0"), parseEther("0")); + // await feeCalc.deployed(); const addresses = [ - feeCalc.address, // fee splitter + ethers.constants.AddressZero, + //feeCalc.address, // fee splitter sgEth.address, // sgETH address wsgEth.address, // wsgETH address multiSig.address, // government address diff --git a/test/v2/core/rewardsReceiver.spec.js b/test/v2/core/rewardsReceiver.spec.js index a74eb64..8538343 100644 --- a/test/v2/core/rewardsReceiver.spec.js +++ b/test/v2/core/rewardsReceiver.spec.js @@ -40,12 +40,13 @@ describe("RewardsReceiver", () => { const numValidators = 1000; const adminFee = 0; - const FeeCalc = await ethers.getContractFactory("FeeCalc"); - const feeCalc = await FeeCalc.deploy(parseEther("0.1"), parseEther("0.1")); - await feeCalc.deployed(); + // const FeeCalc = await ethers.getContractFactory("FeeCalc"); + // const feeCalc = await FeeCalc.deploy(parseEther("0.1"), parseEther("0.1")); + // await feeCalc.deployed(); const addresses = [ - feeCalc.address, // fee splitter + ethers.constants.AddressZero, + // feeCalc.address, // fee splitter sgEth.address, // sgETH address wsgEth.address, // wsgETH address multiSig.address, // government address From f90f060359e3c56bb01ab367591741062cd3f3bf Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Thu, 6 Jun 2024 07:08:34 -0700 Subject: [PATCH 07/13] redeem rate test --- contracts/lib/xERC4626.sol | 128 ++++++++++++++++++------------------ package.json | 1 + test/v2/core/wsgETH.spec.js | 86 ++++++++++++++++++++++-- yarn.lock | 9 ++- 4 files changed, 154 insertions(+), 70 deletions(-) diff --git a/contracts/lib/xERC4626.sol b/contracts/lib/xERC4626.sol index f2ff33e..c1bda4a 100644 --- a/contracts/lib/xERC4626.sol +++ b/contracts/lib/xERC4626.sol @@ -12,9 +12,9 @@ import {IxERC4626} from "../interfaces/IxERC4626.sol"; // Rewards logic inspired by xERC20 (https://github.com/ZeframLou/playpen/blob/main/src/xERC20.sol) -/** +/** @title An xERC4626 Single Staking Contract -@notice This contract allows users to autocompound rewards denominated in an underlying reward token. +@notice This contract allows users to autocompound rewards denominated in an underlying reward token. It is fully compatible with [ERC4626](https://eips.ethereum.org/EIPS/eip-4626) allowing for DeFi composability. It maintains balances using internal accounting to prevent instantaneous changes in the exchange rate. NOTE: an exception is at contract creation, when a reward cycle begins before the first deposit. After the first deposit, exchange rate updates smoothly. @@ -22,85 +22,85 @@ import {IxERC4626} from "../interfaces/IxERC4626.sol"; Operates on "cycles" which distribute the rewards surplus over the internal balance to users linearly over the remainder of the cycle window. */ abstract contract xERC4626 is IxERC4626, ERC4626 { - using SafeCastLib for *; + using SafeCastLib for *; - /// @notice the maximum length of a rewards cycle - uint32 public immutable rewardsCycleLength; + /// @notice the maximum length of a rewards cycle + uint32 public immutable rewardsCycleLength; - /// @notice the effective start of the current cycle - uint32 public lastSync; + /// @notice the effective start of the current cycle + uint32 public lastSync; - /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. - uint32 public rewardsCycleEnd; + /// @notice the end of the current cycle. Will always be evenly divisible by `rewardsCycleLength`. + uint32 public rewardsCycleEnd; - /// @notice the amount of rewards distributed in a the most recent cycle. - uint192 public lastRewardAmount; + /// @notice the amount of rewards distributed in a the most recent cycle. + uint192 public lastRewardAmount; - uint256 internal storedTotalAssets; + uint256 internal storedTotalAssets; - constructor(uint32 _rewardsCycleLength) { - rewardsCycleLength = _rewardsCycleLength; - // seed initial rewardsCycleEnd - rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength; - } + constructor(uint32 _rewardsCycleLength) { + rewardsCycleLength = _rewardsCycleLength; + // seed initial rewardsCycleEnd + rewardsCycleEnd = (block.timestamp.safeCastTo32() / rewardsCycleLength) * rewardsCycleLength; + } - /// @notice Compute the amount of tokens available to share holders. - /// Increases linearly during a reward distribution period from the sync call, not the cycle start. - function totalAssets() public view override returns (uint256) { - // cache global vars - uint256 storedTotalAssets_ = storedTotalAssets; - uint192 lastRewardAmount_ = lastRewardAmount; - uint32 rewardsCycleEnd_ = rewardsCycleEnd; - uint32 lastSync_ = lastSync; + /// @notice Compute the amount of tokens available to share holders. + /// Increases linearly during a reward distribution period from the sync call, not the cycle start. + function totalAssets() public view override returns (uint256) { + // cache global vars + uint256 storedTotalAssets_ = storedTotalAssets; + uint192 lastRewardAmount_ = lastRewardAmount; + uint32 rewardsCycleEnd_ = rewardsCycleEnd; + uint32 lastSync_ = lastSync; + + if (block.timestamp >= rewardsCycleEnd_) { + // no rewards or rewards fully unlocked + // entire reward amount is available + return storedTotalAssets_ + lastRewardAmount_; + } + + // rewards not fully unlocked + // add unlocked rewards to stored total + uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_); + return storedTotalAssets_ + unlockedRewards; + } - if (block.timestamp >= rewardsCycleEnd_) { - // no rewards or rewards fully unlocked - // entire reward amount is available - return storedTotalAssets_ + lastRewardAmount_; + // Update storedTotalAssets on withdraw/redeem + function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override { + super.beforeWithdraw(amount, shares); + storedTotalAssets -= amount; } - // rewards not fully unlocked - // add unlocked rewards to stored total - uint256 unlockedRewards = (lastRewardAmount_ * (block.timestamp - lastSync_)) / (rewardsCycleEnd_ - lastSync_); - return storedTotalAssets_ + unlockedRewards; - } + // Update storedTotalAssets on deposit/mint + function afterDeposit(uint256 amount, uint256 shares) internal virtual override { + storedTotalAssets += amount; + super.afterDeposit(amount, shares); + } - // Update storedTotalAssets on withdraw/redeem - function beforeWithdraw(uint256 amount, uint256 shares) internal virtual override { - super.beforeWithdraw(amount, shares); - storedTotalAssets -= amount; - } + /// @notice Distributes rewards to xERC4626 holders. + /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle. + function syncRewards() public virtual { + uint192 lastRewardAmount_ = lastRewardAmount; + uint32 timestamp = block.timestamp.safeCastTo32(); - // Update storedTotalAssets on deposit/mint - function afterDeposit(uint256 amount, uint256 shares) internal virtual override { - storedTotalAssets += amount; - super.afterDeposit(amount, shares); - } + if (timestamp < rewardsCycleEnd) revert SyncError(); - /// @notice Distributes rewards to xERC4626 holders. - /// All surplus `asset` balance of the contract over the internal balance becomes queued for the next cycle. - function syncRewards() public virtual { - uint192 lastRewardAmount_ = lastRewardAmount; - uint32 timestamp = block.timestamp.safeCastTo32(); + uint256 storedTotalAssets_ = storedTotalAssets; + uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; - if (timestamp < rewardsCycleEnd) revert SyncError(); + storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE - uint256 storedTotalAssets_ = storedTotalAssets; - uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; + uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; - storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE + if (end - timestamp < rewardsCycleLength / 20) { + end += rewardsCycleLength; + } - uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; + // Combined single SSTORE + lastRewardAmount = nextRewards.safeCastTo192(); + lastSync = timestamp; + rewardsCycleEnd = end; - if (end - timestamp < rewardsCycleLength / 20) { - end += rewardsCycleLength; + emit NewRewardsCycle(end, nextRewards); } - - // Combined single SSTORE - lastRewardAmount = nextRewards.safeCastTo192(); - lastSync = timestamp; - rewardsCycleEnd = end; - - emit NewRewardsCycle(end, nextRewards); - } } diff --git a/package.json b/package.json index 6deced3..453ab11 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@fei-protocol/erc4626": "^0.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.11", "@openzeppelin/contracts": "^4.8.0", "@openzeppelin/contracts-upgradeable": "^4.3.1", "dotenv": "^16.1.4", diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js index faa7e95..dadd88d 100644 --- a/test/v2/core/wsgETH.spec.js +++ b/test/v2/core/wsgETH.spec.js @@ -1,13 +1,14 @@ const {ethers} = require("hardhat"); const {expect} = require("chai"); -const {parseEther} = require("ethers/lib/utils"); +const {parseEther, formatEther} = require("ethers/lib/utils"); +const {time} = require("@nomicfoundation/hardhat-network-helpers"); describe.only("WsgETH.sol", () => { - let sgEth, wsgEth, deployer, alice; + let sgEth, wsgEth, deployer, alice, multiSig; let MINTER_ROLE; beforeEach(async () => { - const [owner, addr1] = await ethers.getSigners(); + const [owner, addr1, addr2] = await ethers.getSigners(); const SgETH = await ethers.getContractFactory("SgETH"); sgEth = await SgETH.deploy([]); @@ -15,6 +16,7 @@ describe.only("WsgETH.sol", () => { deployer = owner; alice = addr1; + multiSig = addr2; MINTER_ROLE = await sgEth.MINTER(); @@ -56,7 +58,7 @@ describe.only("WsgETH.sol", () => { await expect(wsgEth.withdraw(parseEther("1"), alice.address, alice.address)).to.be.revertedWith(""); // panic revert by insufficient balance - // mint wsgEth to alice + // approve sgEth to alice await sgEth.approve(wsgEth.address, parseEther("1")); await wsgEth.deposit(parseEther("1"), alice.address); @@ -71,7 +73,7 @@ describe.only("WsgETH.sol", () => { await expect(wsgEth.redeem(parseEther("1"), alice.address, alice.address)).to.be.revertedWith(""); // panic revert by insufficient balance - // mint wsgEth to alice + // approve sgEth to alice await sgEth.approve(wsgEth.address, parseEther("1")); await wsgEth.deposit(parseEther("1"), alice.address); @@ -119,4 +121,78 @@ describe.only("WsgETH.sol", () => { .to.be.emit(wsgEth, "Transfer") .withArgs(ethers.constants.AddressZero, alice.address, parseEther("1")); }); + + it("price per share", async () => { + const splitterAddresses = [deployer.address, multiSig.address, wsgEth.address]; + const splitterValues = [6, 3, 31]; + + const PaymentSplitter = await ethers.getContractFactory("PaymentSplitter"); + const paymentSplitter = await PaymentSplitter.deploy(splitterAddresses, splitterValues); + await paymentSplitter.deployed(); + + const rolloverVirtual = "1080000000000000000"; + const vETH2Addr = "0x898bad2774eb97cf6b94605677f43b41871410b1"; + + const Withdrawals = await ethers.getContractFactory("Withdrawals"); + const withdrawals = await Withdrawals.deploy(vETH2Addr, rolloverVirtual); + await withdrawals.deployed(); + + const numValidators = 1000; + const adminFee = 0; + + const addresses = [ + ethers.constants.AddressZero, // fee splitter + sgEth.address, // sgETH address + wsgEth.address, // wsgETH address + multiSig.address, // government address + ethers.constants.AddressZero, // deposit contract address - can't find deposit contract - using dummy address + ]; + + // add secondary minter contract / eoa + const Minter = await ethers.getContractFactory("SharedDepositMinterV2"); + const minter = await Minter.deploy(numValidators, adminFee, addresses); + await minter.deployed(); + + const RewardsReceiver = await ethers.getContractFactory("RewardsReceiver"); + const rewardsReceiver = await RewardsReceiver.deploy(withdrawals.address, [ + sgEth.address, + wsgEth.address, + paymentSplitter.address, + minter.address, + ]); + await rewardsReceiver.deployed(); + + await sgEth.addMinter(minter.address); + + // approve sgEth to alice + await sgEth.approve(wsgEth.address, parseEther("2")); + await wsgEth.deposit(parseEther("2"), alice.address); + + console.log(formatEther(await wsgEth.pricePerShare())); + + await expect(wsgEth.connect(alice).redeem(parseEther("0.5"), alice.address, alice.address)) + .to.be.emit(wsgEth, "Withdraw") + .withArgs(alice.address, alice.address, alice.address, parseEther("0.5"), parseEther("0.5")); + + // deposit to rewardReceiver to simulate reward + await deployer.sendTransaction({ + to: rewardsReceiver.address, + value: parseEther("1"), + }); + // sends 60% of sgEth to WSGEth contract - so current rate is 1.5/2.1 + console.log(await rewardsReceiver.state()); + await rewardsReceiver.work(); + + await expect(wsgEth.syncRewards()).to.be.revertedWith("SyncError()"); + // increase time by reward cycle + await time.increase(24 * 60 * 60); + await wsgEth.syncRewards(); + + await time.increase(24 * 60 * 60); + + // redeem will get 1.1 sgEth + await expect(wsgEth.connect(alice).redeem(parseEther("0.5"), alice.address, alice.address)) + .to.be.emit(wsgEth, "Withdraw") + .withArgs(alice.address, alice.address, alice.address, parseEther("0.7"), parseEther("0.5")); + }); }); diff --git a/yarn.lock b/yarn.lock index f6e164a..93ad908 100644 --- a/yarn.lock +++ b/yarn.lock @@ -726,6 +726,13 @@ "@nomicfoundation/ethereumjs-rlp" "5.0.4" ethereum-cryptography "0.1.3" +"@nomicfoundation/hardhat-network-helpers@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.11.tgz#64096829661b960b88679bd5c4fbcb50654672d1" + integrity sha512-uGPL7QSKvxrHRU69dx8jzoBvuztlLCtyFsbgfXIwIjnO3dqZRz2GNMHJoO3C3dIiUNM6jdNF4AUnoQKDscdYrA== + dependencies: + ethereumjs-util "^7.1.4" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz#4c858096b1c17fe58a474fe81b46815f93645c15" @@ -4310,7 +4317,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3: +ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.4: version "7.1.5" resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz" integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== From 753c1ffe1e46fbf6db311d59d5e3d00d40040176 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Fri, 7 Jun 2024 19:03:44 -0700 Subject: [PATCH 08/13] removed console log --- test/v2/core/wsgETH.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js index dadd88d..c8956f5 100644 --- a/test/v2/core/wsgETH.spec.js +++ b/test/v2/core/wsgETH.spec.js @@ -168,8 +168,6 @@ describe.only("WsgETH.sol", () => { await sgEth.approve(wsgEth.address, parseEther("2")); await wsgEth.deposit(parseEther("2"), alice.address); - console.log(formatEther(await wsgEth.pricePerShare())); - await expect(wsgEth.connect(alice).redeem(parseEther("0.5"), alice.address, alice.address)) .to.be.emit(wsgEth, "Withdraw") .withArgs(alice.address, alice.address, alice.address, parseEther("0.5"), parseEther("0.5")); @@ -180,7 +178,6 @@ describe.only("WsgETH.sol", () => { value: parseEther("1"), }); // sends 60% of sgEth to WSGEth contract - so current rate is 1.5/2.1 - console.log(await rewardsReceiver.state()); await rewardsReceiver.work(); await expect(wsgEth.syncRewards()).to.be.revertedWith("SyncError()"); From 6e79583862d2c443879d260c35751267273e7829 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Fri, 7 Jun 2024 19:04:47 -0700 Subject: [PATCH 09/13] added comment --- test/v2/core/wsgETH.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js index c8956f5..dd9b89b 100644 --- a/test/v2/core/wsgETH.spec.js +++ b/test/v2/core/wsgETH.spec.js @@ -187,7 +187,7 @@ describe.only("WsgETH.sol", () => { await time.increase(24 * 60 * 60); - // redeem will get 1.1 sgEth + // redeem will get 0.7 = 2.1/1.5*0.5 sgEth await expect(wsgEth.connect(alice).redeem(parseEther("0.5"), alice.address, alice.address)) .to.be.emit(wsgEth, "Withdraw") .withArgs(alice.address, alice.address, alice.address, parseEther("0.7"), parseEther("0.5")); From 3638424656887086395095b4f59c9d5376fc0888 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Sun, 9 Jun 2024 11:11:15 -0700 Subject: [PATCH 10/13] removed manual syncReward call --- test/v2/core/wsgETH.spec.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js index dd9b89b..a116b88 100644 --- a/test/v2/core/wsgETH.spec.js +++ b/test/v2/core/wsgETH.spec.js @@ -183,13 +183,19 @@ describe.only("WsgETH.sol", () => { await expect(wsgEth.syncRewards()).to.be.revertedWith("SyncError()"); // increase time by reward cycle await time.increase(24 * 60 * 60); - await wsgEth.syncRewards(); + // await wsgEth.syncRewards(); + // reward rate is not updated yet + // after call this functions, reward rate is increasing linearly from 1 to 1.0/(2.1-0.5) = 1.0/1.6 + await expect(wsgEth.connect(alice).redeem(parseEther("0.5"), alice.address, alice.address)) + .to.be.emit(wsgEth, "Withdraw") + .withArgs(alice.address, alice.address, alice.address, parseEther("0.5"), parseEther("0.5")); await time.increase(24 * 60 * 60); - // redeem will get 0.7 = 2.1/1.5*0.5 sgEth + // after reward cycle, reward rate is updated with new rate + // redeem will get 0.8 = 1.6/1.0*0.5 sgEth await expect(wsgEth.connect(alice).redeem(parseEther("0.5"), alice.address, alice.address)) .to.be.emit(wsgEth, "Withdraw") - .withArgs(alice.address, alice.address, alice.address, parseEther("0.7"), parseEther("0.5")); + .withArgs(alice.address, alice.address, alice.address, parseEther("0.8"), parseEther("0.5")); }); }); From 872720cc147eb54e501f6dd5eff2068552a0aef7 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Sun, 9 Jun 2024 12:02:45 -0700 Subject: [PATCH 11/13] feedback processed --- test/v2/core/minter.spec.js | 286 +++++++++++++++++++++--------------- 1 file changed, 170 insertions(+), 116 deletions(-) diff --git a/test/v2/core/minter.spec.js b/test/v2/core/minter.spec.js index 776c330..6c3016f 100644 --- a/test/v2/core/minter.spec.js +++ b/test/v2/core/minter.spec.js @@ -1,12 +1,13 @@ const {ethers} = require("hardhat"); const {expect} = require("chai"); const {parseEther} = require("ethers/lib/utils"); +const {time} = require("@nomicfoundation/hardhat-network-helpers"); describe("SharedDepositMinterV2", () => { - let sgEth, paymentSplitter, minter, withdrawals, wsgEth, deployer, alice, multiSig; + let sgEth, paymentSplitter, minter, withdrawals, wsgEth, rewardsReceiver, deployer, alice, bob, multiSig; beforeEach(async () => { - const [owner, addr1, addr2] = await ethers.getSigners(); + const [owner, addr1, addr2, addr3] = await ethers.getSigners(); const SgETH = await ethers.getContractFactory("SgETH"); sgEth = await SgETH.deploy([]); @@ -14,7 +15,8 @@ describe("SharedDepositMinterV2", () => { deployer = owner; alice = addr1; - multiSig = addr2; + bob = addr2; + multiSig = addr3; MINTER_ROLE = await sgEth.MINTER(); @@ -58,155 +60,207 @@ describe("SharedDepositMinterV2", () => { minter = await Minter.deploy(numValidators, adminFee, addresses); await minter.deployed(); - // const RewardsReceiver = await ethers.getContractFactory("RewardsReceiver"); - // rewardsReceiver = await RewardsReceiver.deploy(withdrawals.address, [ - // sgEth.address, - // wsgEth.address, - // paymentSplitter.address, - // minter.address, - // ]); - // await rewardsReceiver.deployed(); + const RewardsReceiver = await ethers.getContractFactory("RewardsReceiver"); + rewardsReceiver = await RewardsReceiver.deploy(withdrawals.address, [ + sgEth.address, + wsgEth.address, + paymentSplitter.address, + minter.address, + ]); + await rewardsReceiver.deployed(); await sgEth.addMinter(minter.address); }); - it("deposit", async () => { - const prevBalance = await sgEth.balanceOf(alice.address); - await minter.connect(alice).deposit({ - value: parseEther("1"), + describe("functionality", () => { + it("deposit", async () => { + const prevBalance = await sgEth.balanceOf(alice.address); + await minter.connect(alice).deposit({ + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(alice.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); }); - const afterBalance = await sgEth.balanceOf(alice.address); - expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); - }); - it("depositFor", async () => { - const prevBalance = await sgEth.balanceOf(alice.address); - await minter.depositFor(alice.address, { - value: parseEther("1"), + it("depositFor", async () => { + // alice deposit for bob + const prevBalance = await sgEth.balanceOf(bob.address); + await minter.connect(alice).depositFor(bob.address, { + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(bob.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); }); - const afterBalance = await sgEth.balanceOf(alice.address); - expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); - }); - it("depositAndStake", async () => { - const prevStake = await wsgEth.maxRedeem(deployer.address); - const prevBalance = await sgEth.balanceOf(wsgEth.address); - await minter.depositAndStake({ - value: parseEther("1"), + it("depositAndStake", async () => { + const prevStake = await wsgEth.maxRedeem(deployer.address); + const prevBalance = await sgEth.balanceOf(wsgEth.address); + await minter.depositAndStake({ + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(wsgEth.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); + + const afterStake = await wsgEth.maxRedeem(deployer.address); + expect(afterStake).to.eq(prevStake.add(parseEther("1"))); }); - const afterBalance = await sgEth.balanceOf(wsgEth.address); - expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); - const afterStake = await wsgEth.maxRedeem(deployer.address); - expect(afterStake).to.eq(prevStake.add(parseEther("1"))); - }); + it("depositAndStakeFor", async () => { + // alice deposit and stake for bob + const prevStake = await wsgEth.maxRedeem(bob.address); + const prevBalance = await sgEth.balanceOf(wsgEth.address); + await minter.connect(alice).depositAndStakeFor(bob.address, { + value: parseEther("1"), + }); + const afterBalance = await sgEth.balanceOf(wsgEth.address); + expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); + + const afterStake = await wsgEth.maxRedeem(bob.address); + expect(afterStake).to.eq(prevStake.add(parseEther("1"))); + }); + + it("withdraw, withdrawTo", async () => { + await minter.connect(alice).deposit({ + value: parseEther("1"), + }); + await expect(minter.connect(alice).withdraw(parseEther("1.1"))).to.be.revertedWith(""); + + let prevBalance = await sgEth.balanceOf(alice.address); + await minter.connect(alice).withdraw(parseEther("0.5")); + let afterBalance = await sgEth.balanceOf(alice.address); - it("depositAndStakeFor", async () => { - const prevStake = await wsgEth.maxRedeem(alice.address); - const prevBalance = await sgEth.balanceOf(wsgEth.address); - await minter.depositAndStakeFor(alice.address, { - value: parseEther("1"), + expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); + + prevBalance = await sgEth.balanceOf(alice.address); + await minter.connect(alice).withdrawTo(parseEther("0.5"), bob.address); + afterBalance = await sgEth.balanceOf(alice.address); + + expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); }); - const afterBalance = await sgEth.balanceOf(wsgEth.address); - expect(afterBalance).to.eq(prevBalance.add(parseEther("1"))); - const afterStake = await wsgEth.maxRedeem(alice.address); - expect(afterStake).to.eq(prevStake.add(parseEther("1"))); - }); + it("unstakeAndWithdraw", async () => { + await minter.connect(alice).depositAndStake({ + value: parseEther("1"), + }); + await expect(minter.connect(alice).unstakeAndWithdraw(parseEther("1.1"), alice.address)).to.be.revertedWith(""); + await expect(minter.connect(alice).unstakeAndWithdraw(parseEther("0.5"), alice.address)).to.be.revertedWith(""); - it("withdraw, withdrawTo", async () => { - await minter.connect(alice).deposit({ - value: parseEther("1"), + await wsgEth.connect(alice).approve(minter.address, ethers.constants.MaxUint256); + let prevBalance = await wsgEth.balanceOf(alice.address); + await minter.connect(alice).unstakeAndWithdraw(parseEther("0.5"), alice.address); + let afterBalance = await wsgEth.balanceOf(alice.address); + + expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); }); - await expect(minter.connect(alice).withdraw(parseEther("1.1"))).to.be.revertedWith(""); - let prevBalance = await sgEth.balanceOf(alice.address); - await minter.connect(alice).withdraw(parseEther("0.5")); - let afterBalance = await sgEth.balanceOf(alice.address); + it("slash", async () => { + await minter.connect(alice).depositAndStake({ + value: parseEther("10"), + }); + expect(await wsgEth.maxWithdraw(alice.address)).to.eq(parseEther("10")); - expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); + // deposit to rewardReceiver to simulate reward + await deployer.sendTransaction({ + to: rewardsReceiver.address, + value: parseEther("1"), + }); + // sends 60% of sgEth to WSGEth contract + await rewardsReceiver.work(); - prevBalance = await sgEth.balanceOf(alice.address); - await minter.connect(alice).withdrawTo(parseEther("0.5"), alice.address); - afterBalance = await sgEth.balanceOf(alice.address); + // slash 0.1 eth from reward amount, so currently reward is 0.5 + await expect(minter.connect(multiSig).slash(parseEther("0.1"))) + .to.be.emit(sgEth, "Transfer") + .withArgs(wsgEth.address, ethers.constants.AddressZero, parseEther("0.1")); - expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); - }); + // max withdrawal amount is not changed yet because didn't called syncReward of WsgEth + expect(await wsgEth.maxWithdraw(alice.address)).to.eq(parseEther("10")); - it("unstakeAndWithdraw", async () => { - await minter.connect(alice).depositAndStake({ - value: parseEther("1"), - }); - await expect(minter.connect(alice).unstakeAndWithdraw(parseEther("1.1"), alice.address)).to.be.revertedWith(""); - await expect(minter.connect(alice).unstakeAndWithdraw(parseEther("0.5"), alice.address)).to.be.revertedWith(""); + await time.increase(24 * 60 * 60); - await wsgEth.connect(alice).approve(minter.address, ethers.constants.MaxUint256); - let prevBalance = await wsgEth.balanceOf(alice.address); - await minter.connect(alice).unstakeAndWithdraw(parseEther("0.5"), alice.address); - let afterBalance = await wsgEth.balanceOf(alice.address); + // deposit more to call syncReward, rewards increases by linear from this moment + await minter.connect(alice).depositAndStake({ + value: parseEther("1"), + }); - expect(afterBalance).to.eq(prevBalance.sub(parseEther("0.5"))); - }); + await time.increase(24 * 60 * 60); - it("setWithdrawalCredential", async () => { - const NOR_ROLE = await minter.NOR(); - await expect(minter.connect(alice).setWithdrawalCredential("0x")).to.be.revertedWith( - `AccessControl: account ${alice.address.toLowerCase()} is missing role ${NOR_ROLE}`, - ); + // deposit more to call syncReward. can test with full reward + await minter.connect(alice).depositAndStake({ + value: parseEther("1"), + }); - await minter.setWithdrawalCredential("0x"); + // currently withdrawal amount is 10 + 1 + 1 + 0.6 - 0.1 = 12.5 + expect(await wsgEth.maxWithdraw(alice.address)).to.eq(parseEther("12.5")); + }); }); - it("slash", async () => { - const GOV_ROLE = await minter.GOV(); - await expect(minter.connect(alice).slash(parseEther("0.1"))).to.be.revertedWith( - `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, - ); + describe("access control", async () => { + it("setWithdrawalCredential", async () => { + // only NOR Role can call this function + const NOR_ROLE = await minter.NOR(); + await expect(minter.connect(alice).setWithdrawalCredential("0x")).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${NOR_ROLE}`, + ); - await expect(minter.connect(multiSig).slash(parseEther("0.1"))).to.be.revertedWith("AmountTooHigh()"); + await minter.setWithdrawalCredential("0x"); + }); - await minter.connect(alice).depositAndStake({ - value: parseEther("10"), + it("slash", async () => { + // only GOV Role can call this function + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).slash(parseEther("0.1"))).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + + await expect(minter.connect(multiSig).slash(parseEther("0.1"))).to.be.revertedWith("AmountTooHigh()"); + + await minter.connect(alice).depositAndStake({ + value: parseEther("10"), + }); + await expect(minter.connect(multiSig).slash(parseEther("0.1"))) + .to.be.emit(sgEth, "Transfer") + .withArgs(wsgEth.address, ethers.constants.AddressZero, parseEther("0.1")); }); - await expect(minter.connect(multiSig).slash(parseEther("0.1"))) - .to.be.emit(sgEth, "Transfer") - .withArgs(wsgEth.address, ethers.constants.AddressZero, parseEther("0.1")); - }); - it("togglePause", async () => { - const GOV_ROLE = await minter.GOV(); - await expect(minter.connect(alice).togglePause()).to.be.revertedWith( - `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, - ); + it("togglePause", async () => { + // only GOV Role can call this function + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).togglePause()).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); - await expect(minter.connect(multiSig).togglePause()).to.be.emit(minter, "Paused").withArgs(multiSig.address); - }); + await expect(minter.connect(multiSig).togglePause()).to.be.emit(minter, "Paused").withArgs(multiSig.address); + }); - it("migrateShares", async () => { - const GOV_ROLE = await minter.GOV(); - await expect(minter.connect(alice).migrateShares(parseEther("0.1"))).to.be.revertedWith( - `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, - ); + it("migrateShares", async () => { + // only GOV Role can call this function + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).migrateShares(parseEther("0.1"))).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); - await minter.connect(multiSig).migrateShares(parseEther("0.1")); - }); + await minter.connect(multiSig).migrateShares(parseEther("0.1")); + }); - it("toggleWithdrawRefund", async () => { - const GOV_ROLE = await minter.GOV(); - await expect(minter.connect(alice).toggleWithdrawRefund()).to.be.revertedWith( - `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, - ); + it("toggleWithdrawRefund", async () => { + // only GOV Role can call this function + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).toggleWithdrawRefund()).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); - await minter.connect(multiSig).toggleWithdrawRefund(); - }); + await minter.connect(multiSig).toggleWithdrawRefund(); + }); - it("setNumValidators", async () => { - const GOV_ROLE = await minter.GOV(); - await expect(minter.connect(alice).setNumValidators(1)).to.be.revertedWith( - `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, - ); - await expect(minter.connect(multiSig).setNumValidators(0)).to.be.revertedWith("Minimum 1 validator"); + it("setNumValidators", async () => { + // only GOV Role can call this function + const GOV_ROLE = await minter.GOV(); + await expect(minter.connect(alice).setNumValidators(1)).to.be.revertedWith( + `AccessControl: account ${alice.address.toLowerCase()} is missing role ${GOV_ROLE}`, + ); + await expect(minter.connect(multiSig).setNumValidators(0)).to.be.revertedWith("Minimum 1 validator"); - await minter.connect(multiSig).setNumValidators(1); + await minter.connect(multiSig).setNumValidators(1); + }); }); }); From d25825a0d199ce933ec320714a0ca4682129ef50 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Sun, 9 Jun 2024 20:20:21 -0700 Subject: [PATCH 12/13] added to dependencies --- package.json | 1 + yarn.lock | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6deced3..453ab11 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@fei-protocol/erc4626": "^0.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.11", "@openzeppelin/contracts": "^4.8.0", "@openzeppelin/contracts-upgradeable": "^4.3.1", "dotenv": "^16.1.4", diff --git a/yarn.lock b/yarn.lock index f6e164a..93ad908 100644 --- a/yarn.lock +++ b/yarn.lock @@ -726,6 +726,13 @@ "@nomicfoundation/ethereumjs-rlp" "5.0.4" ethereum-cryptography "0.1.3" +"@nomicfoundation/hardhat-network-helpers@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.11.tgz#64096829661b960b88679bd5c4fbcb50654672d1" + integrity sha512-uGPL7QSKvxrHRU69dx8jzoBvuztlLCtyFsbgfXIwIjnO3dqZRz2GNMHJoO3C3dIiUNM6jdNF4AUnoQKDscdYrA== + dependencies: + ethereumjs-util "^7.1.4" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz#4c858096b1c17fe58a474fe81b46815f93645c15" @@ -4310,7 +4317,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3: +ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.4: version "7.1.5" resolved "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz" integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg== From 135139eb3ae5d67ba81d231a9be4e2283c159823 Mon Sep 17 00:00:00 2001 From: devlancer412 Date: Sun, 9 Jun 2024 20:30:16 -0700 Subject: [PATCH 13/13] removed testing cases --- test/v2/core/wsgETH.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/v2/core/wsgETH.spec.js b/test/v2/core/wsgETH.spec.js index a116b88..013d7a6 100644 --- a/test/v2/core/wsgETH.spec.js +++ b/test/v2/core/wsgETH.spec.js @@ -3,7 +3,7 @@ const {expect} = require("chai"); const {parseEther, formatEther} = require("ethers/lib/utils"); const {time} = require("@nomicfoundation/hardhat-network-helpers"); -describe.only("WsgETH.sol", () => { +describe("WsgETH.sol", () => { let sgEth, wsgEth, deployer, alice, multiSig; let MINTER_ROLE; @@ -85,7 +85,7 @@ describe.only("WsgETH.sol", () => { it("depositWithSignature", async () => { await sgEth.transfer(alice.address, parseEther("1")); const nonce = await sgEth.nonces(alice.address); - const deadline = Math.floor(Date.now() / 1000) + 1000; + const deadline = Math.floor(Date.now() / 1000) + 1000000; const approveData = { owner: alice.address, spender: wsgEth.address,