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

wsg eth test finished #21

Closed
wants to merge 7 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
128 changes: 64 additions & 64 deletions contracts/lib/xERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,95 +12,95 @@ 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.

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);
}
}
2 changes: 1 addition & 1 deletion contracts/v2/core/WSGEth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
201 changes: 201 additions & 0 deletions test/v2/core/wsgETH.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
const {ethers} = require("hardhat");
const {expect} = require("chai");
const {parseEther, formatEther} = require("ethers/lib/utils");
const {time} = require("@nomicfoundation/hardhat-network-helpers");

describe.only("WsgETH.sol", () => {
let sgEth, wsgEth, deployer, alice, multiSig;
let MINTER_ROLE;

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 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

// approve sgEth 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

// approve sgEth 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"));
});

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);

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
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();
// 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);

// 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.8"), parseEther("0.5"));
});
});
Loading
Loading