From 57e5add1d07a2afd48c71bf88cd494e73d810310 Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 00:44:19 +0100 Subject: [PATCH 01/10] add ampleforth geyser v2 tests --- test/Access/ERC1271.ts | 47 + test/Geyser.ts | 2416 +++++++++++++++++++++++++++++++ test/PowerSwitch/PowerSwitch.ts | 103 ++ test/PowerSwitch/Powered.ts | 99 ++ test/RewardPool.ts | 124 ++ test/UniversalVault.ts | 609 ++++++++ test/VaultFactory.ts | 43 + test/utils.ts | 133 ++ 8 files changed, 3574 insertions(+) create mode 100644 test/Access/ERC1271.ts create mode 100644 test/Geyser.ts create mode 100644 test/PowerSwitch/PowerSwitch.ts create mode 100644 test/PowerSwitch/Powered.ts create mode 100644 test/RewardPool.ts create mode 100644 test/UniversalVault.ts create mode 100644 test/VaultFactory.ts create mode 100644 test/utils.ts diff --git a/test/Access/ERC1271.ts b/test/Access/ERC1271.ts new file mode 100644 index 0000000..233c769 --- /dev/null +++ b/test/Access/ERC1271.ts @@ -0,0 +1,47 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { hashMessage, keccak256 } from 'ethers/lib/utils' +import { ethers } from 'hardhat' +import { deployContract } from '../utils' + +describe('ERC1271', function () { + let accounts: SignerWithAddress[] + let MockERC1271: Contract + const message = hashMessage('ERC1271 test message') + let VALID_SIG: string + const INVALID_SIG = '0x00000000' + + function toEthSignedMessageHash(messageHex: string) { + const messageBuffer = Buffer.from(messageHex.substring(2), 'hex') + const prefix = Buffer.from(`\u0019Ethereum Signed Message:\n${messageBuffer.length}`) + return keccak256(Buffer.concat([prefix, messageBuffer])) + } + + beforeEach(async function () { + // prepare signers + accounts = await ethers.getSigners() + // deploy mock + MockERC1271 = await deployContract('MockERC1271', [accounts[0].address]) + VALID_SIG = MockERC1271.interface.getSighash('isValidSignature(bytes32,bytes)') + }) + + describe('isValidSignature', function () { + it('should return error value if signed by account other than owner', async function () { + const sig = await accounts[1].signMessage(message) + expect(await MockERC1271.isValidSignature(toEthSignedMessageHash(message), sig)).to.eq(INVALID_SIG) + }) + + it('should revert if signature has incorrect length', async function () { + const sig = await accounts[0].signMessage(message) + expect(MockERC1271.isValidSignature(toEthSignedMessageHash(message), sig.slice(0, 10))).to.be.revertedWith( + 'ECDSA: invalid signature length', + ) + }) + + it('should return success value if signed by owner', async function () { + const sig = await accounts[0].signMessage(message) + expect(await MockERC1271.isValidSignature(hashMessage(message), sig)).to.eq(VALID_SIG) + }) + }) +}) diff --git a/test/Geyser.ts b/test/Geyser.ts new file mode 100644 index 0000000..1415ee4 --- /dev/null +++ b/test/Geyser.ts @@ -0,0 +1,2416 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { BigNumber, BigNumberish, Contract, Wallet } from 'ethers' +import { ethers, network } from 'hardhat' +import { + createInstance, + deployAmpl, + deployContract, + deployAludel, + getTimestamp, + increaseTime, + invokeRebase, + signPermission, +} from './utils' + +/* + + Dev note: This test file suffers from indeterminancy based on timestamp manipulation. + + If you are encountering tests which fail unexpectedly, make sure increaseTime() is + correctly setting the timestamp of the next block. + + Issue is being tracked here: https://github.com/nomiclabs/hardhat/issues/1079 + +*/ + +/* + + Note: the implementation of increaseTime() was changed to only use `evm_mine` with the + timestamp passed as a parameter, instead of using a combination of `evm_increaseTime` and `evm_mine` + + This implementation does not seem to run into the same problem as it previously did. + As a result, the indeterminancy issue is fixed + +*/ + +describe('Aludel', function () { + let accounts: SignerWithAddress[], admin: SignerWithAddress + let user: Wallet + + let powerSwitchFactory: Contract, + rewardPoolFactory: Contract, + vaultTemplate: Contract, + vaultFactory: Contract, + stakingToken: Contract, + rewardToken: Contract, + bonusToken: Contract + + const mockTokenSupply = ethers.utils.parseEther('1000') + const BASE_SHARES_PER_WEI = 1000000 + const DAY = 24 * 3600 + const YEAR = 365 * DAY + const rewardScaling = { floor: 33, ceiling: 100, time: 60 * DAY } + + let amplInitialSupply: BigNumber + + const stake = async ( + user: Wallet, + geyser: Contract, + vault: Contract, + stakingToken: Contract, + amount: BigNumberish, + vaultNonce?: BigNumberish, + ) => { + // sign permission + const signedPermission = await signPermission( + 'Lock', + vault, + user, + geyser.address, + stakingToken.address, + amount, + vaultNonce, + ) + // stake on geyser + return geyser.stake(vault.address, amount, signedPermission) + } + + const unstakeAndClaim = async ( + user: Wallet, + geyser: Contract, + vault: Contract, + stakingToken: Contract, + amount: BigNumberish, + vaultNonce?: BigNumberish, + ) => { + // sign permission + const signedPermission = await signPermission( + 'Unlock', + vault, + user, + geyser.address, + stakingToken.address, + amount, + vaultNonce, + ) + // unstake on geyser + return geyser.unstakeAndClaim(vault.address, amount, signedPermission) + } + + function calculateExpectedReward( + stakeAmount: BigNumber, + stakeDuration: BigNumberish, + rewardAvailable: BigNumber, + otherStakeUnits: BigNumberish, + ) { + const stakeUnits = stakeAmount.mul(stakeDuration) + const baseReward = rewardAvailable.mul(stakeUnits).div(stakeUnits.add(otherStakeUnits)) + const minReward = baseReward.mul(rewardScaling.floor).div(100) + const bonusReward = baseReward + .mul(rewardScaling.ceiling - rewardScaling.floor) + .mul(stakeDuration) + .div(rewardScaling.time) + .div(100) + return stakeDuration >= rewardScaling.time ? baseReward : minReward.add(bonusReward) + } + + before(async function () { + // prepare signers + accounts = await ethers.getSigners() + admin = accounts[1] + user = Wallet.createRandom().connect(ethers.provider) + await accounts[2].sendTransaction({ + to: user.address, + value: (await accounts[2].getBalance()).mul(9).div(10), + }) + }) + + beforeEach(async function () { + // deploy dependencies + powerSwitchFactory = await deployContract('PowerSwitchFactory') + rewardPoolFactory = await deployContract('RewardPoolFactory') + vaultTemplate = await deployContract('UniversalVault') + vaultFactory = await deployContract('VaultFactory', [vaultTemplate.address]) + + // deploy mock tokens + stakingToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) + ;({ ampl: rewardToken, amplInitialSupply } = await deployAmpl(admin)) + bonusToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) + }) + + describe('initialize', function () { + describe('when rewardScaling.floor > rewardScaling.ceiling', function () { + it('should fail', async function () { + const args = [ + admin.address, + rewardPoolFactory.address, + powerSwitchFactory.address, + stakingToken.address, + rewardToken.address, + [rewardScaling.ceiling + 1, rewardScaling.ceiling, rewardScaling.time], + ] + await expect(deployAludel(args)).to.be.reverted + }) + }) + describe('when rewardScalingTime = 0', function () { + it('should fail', async function () { + const args = [ + admin.address, + rewardPoolFactory.address, + powerSwitchFactory.address, + stakingToken.address, + rewardToken.address, + [rewardScaling.floor, rewardScaling.ceiling, 0], + ] + await expect(deployAludel(args)).to.be.reverted + }) + }) + describe('when parameters are valid', function () { + it('should set contract variables', async function () { + const args = [ + admin.address, + rewardPoolFactory.address, + powerSwitchFactory.address, + stakingToken.address, + rewardToken.address, + [rewardScaling.floor, rewardScaling.ceiling, rewardScaling.time], + ] + const geyser = await deployAludel(args) + + const data = await geyser.getGeyserData() + + expect(data.stakingToken).to.eq(stakingToken.address) + expect(data.rewardToken).to.eq(rewardToken.address) + expect(data.rewardPool).to.not.eq(ethers.constants.AddressZero) + expect(data.rewardScaling.floor).to.eq(33) + expect(data.rewardScaling.ceiling).to.eq(100) + expect(data.rewardSharesOutstanding).to.eq(0) + expect(data.totalStake).to.eq(0) + expect(data.totalStakeUnits).to.eq(0) + expect(data.lastUpdate).to.eq(0) + expect(data.rewardSchedules).to.deep.eq([]) + expect(await geyser.getBonusTokenSetLength()).to.eq(0) + expect(await geyser.owner()).to.eq(admin.address) + expect(await geyser.getPowerSwitch()).to.not.eq(ethers.constants.AddressZero) + expect(await geyser.getPowerController()).to.eq(admin.address) + expect(await geyser.isOnline()).to.eq(true) + expect(await geyser.isOffline()).to.eq(false) + expect(await geyser.isShutdown()).to.eq(false) + }) + }) + }) + + describe('admin functions', function () { + let geyser: Contract, powerSwitch: Contract, rewardPool: Contract + beforeEach(async function () { + const args = [ + admin.address, + rewardPoolFactory.address, + powerSwitchFactory.address, + stakingToken.address, + rewardToken.address, + [rewardScaling.floor, rewardScaling.ceiling, rewardScaling.time], + ] + geyser = await deployAludel(args) + powerSwitch = await ethers.getContractAt('PowerSwitch', await geyser.getPowerSwitch()) + rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getGeyserData()).rewardPool) + }) + describe('fundGeyser', function () { + describe('with insufficient approval', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)).to.be.reverted + }) + }) + describe('with duration of zero', function () { + it('should fail', async function () { + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, 0)).to.be.revertedWith( + 'Geyser: invalid duration', + ) + }) + }) + describe('as user', function () { + it('should fail', async function () { + await rewardToken.connect(admin).transfer(user.address, amplInitialSupply) + await rewardToken.connect(user).approve(geyser.address, amplInitialSupply) + await expect(geyser.connect(user).fundGeyser(amplInitialSupply, YEAR)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) + }) + }) + describe('when offline', function () { + it('should fail', async function () { + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + await powerSwitch.connect(admin).powerOff() + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('when shutdown', function () { + it('should fail', async function () { + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + await powerSwitch.connect(admin).emergencyShutdown() + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('when online', function () { + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + }) + describe('at first funding', function () { + it('should succeed', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + }) + it('should update state correctly', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + + const data = await geyser.getGeyserData() + + expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) + expect(data.rewardSchedules.length).to.eq(1) + expect(data.rewardSchedules[0].duration).to.eq(YEAR) + expect(data.rewardSchedules[0].start).to.eq(await getTimestamp()) + expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) + .to.emit(geyser, 'GeyserFunded') + .withArgs(amplInitialSupply, YEAR) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) + .to.emit(rewardToken, 'Transfer') + .withArgs(admin.address, rewardPool.address, amplInitialSupply) + }) + }) + describe('at second funding', function () { + beforeEach(async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR) + }) + describe('with no rebase', function () { + it('should succeed', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR) + }) + it('should update state correctly', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR) + + const data = await geyser.getGeyserData() + + expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) + expect(data.rewardSchedules.length).to.eq(2) + expect(data.rewardSchedules[0].duration).to.eq(YEAR) + expect(data.rewardSchedules[0].start).to.eq((await getTimestamp()) - 1) + expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + expect(data.rewardSchedules[1].duration).to.eq(YEAR) + expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) + expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR)) + .to.emit(geyser, 'GeyserFunded') + .withArgs(amplInitialSupply.div(2), YEAR) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR)) + .to.emit(rewardToken, 'Transfer') + .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) + }) + }) + describe('with positive rebase of 200%', function () { + beforeEach(async function () { + // rebase of 100 doubles the inital supply + await invokeRebase(rewardToken, 100, admin) + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + }) + it('should succeed', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + }) + it('should update state correctly', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + + const data = await geyser.getGeyserData() + + expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) + expect(data.rewardSchedules.length).to.eq(2) + expect(data.rewardSchedules[0].duration).to.eq(YEAR) + expect(data.rewardSchedules[0].start).to.eq((await getTimestamp()) - 3) + expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + expect(data.rewardSchedules[1].duration).to.eq(YEAR) + expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) + expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) + .to.emit(geyser, 'GeyserFunded') + .withArgs(amplInitialSupply, YEAR) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) + .to.emit(rewardToken, 'Transfer') + .withArgs(admin.address, rewardPool.address, amplInitialSupply) + }) + }) + describe('with negative rebase of 50%', function () { + beforeEach(async function () { + // rebase of -50 halves the inital supply + await invokeRebase(rewardToken, -50, admin) + }) + it('should succeed', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR) + }) + it('should update state correctly', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR) + + const data = await geyser.getGeyserData() + + expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) + expect(data.rewardSchedules.length).to.eq(2) + expect(data.rewardSchedules[0].duration).to.eq(YEAR) + expect(data.rewardSchedules[0].start).to.eq((await getTimestamp()) - 2) + expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + expect(data.rewardSchedules[1].duration).to.eq(YEAR) + expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) + expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR)) + .to.emit(geyser, 'GeyserFunded') + .withArgs(amplInitialSupply.div(4), YEAR) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR)) + .to.emit(rewardToken, 'Transfer') + .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(4)) + }) + }) + }) + describe('after unstake', function () { + const stakeAmount = ethers.utils.parseEther('100') + + let vault: Contract + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + }) + describe('with partial rewards exausted', function () { + beforeEach(async function () { + await increaseTime(rewardScaling.time / 2) + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should succeed', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + }) + it('should update state correctly', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + + const data = await geyser.getGeyserData() + + expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).mul(3).div(4)) + expect(data.rewardSchedules.length).to.eq(2) + expect(data.rewardSchedules[0].duration).to.eq(rewardScaling.time) + expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + expect(data.rewardSchedules[1].duration).to.eq(rewardScaling.time) + expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) + expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) + .to.emit(geyser, 'GeyserFunded') + .withArgs(amplInitialSupply.div(2), rewardScaling.time) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) + .to.emit(rewardToken, 'Transfer') + .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) + }) + }) + describe('with full rewards exausted', function () { + beforeEach(async function () { + await increaseTime(rewardScaling.time) + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should succeed', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + }) + it('should update state correctly', async function () { + await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + + const data = await geyser.getGeyserData() + + expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + expect(data.rewardSchedules.length).to.eq(2) + expect(data.rewardSchedules[0].duration).to.eq(rewardScaling.time) + expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + expect(data.rewardSchedules[1].duration).to.eq(rewardScaling.time) + expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) + expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) + .to.emit(geyser, 'GeyserFunded') + .withArgs(amplInitialSupply.div(2), rewardScaling.time) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) + .to.emit(rewardToken, 'Transfer') + .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) + }) + }) + }) + }) + }) + + describe('isValidVault', function () { + let vault: Contract + beforeEach(async function () { + vault = await createInstance('UniversalVault', vaultFactory, user) + }) + describe('when no factory registered', function () { + it('should be false', async function () { + expect(await geyser.isValidVault(vault.address)).to.be.false + }) + }) + describe('when vault from factory registered', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should be true', async function () { + expect(await geyser.isValidVault(vault.address)).to.be.true + }) + }) + describe('when vault from factory removed', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + }) + it('should be false', async function () { + expect(await geyser.isValidVault(vault.address)).to.be.false + }) + }) + describe('when vault not from factory registered', function () { + let secondFactory: Contract + let secondVault: Contract + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + secondFactory = await deployContract('VaultFactory', [vaultTemplate.address]) + secondVault = await createInstance('UniversalVault', secondFactory, user) + }) + it('should be false', async function () { + expect(await geyser.isValidVault(secondVault.address)).to.be.false + }) + }) + describe('when vaults from multiple factory registered', function () { + let secondFactory: Contract + let secondVault: Contract + beforeEach(async function () { + secondFactory = await deployContract('VaultFactory', [vaultTemplate.address]) + secondVault = await createInstance('UniversalVault', secondFactory, user) + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + await geyser.connect(admin).registerVaultFactory(secondFactory.address) + }) + it('should be true', async function () { + expect(await geyser.isValidVault(vault.address)).to.be.true + expect(await geyser.isValidVault(secondVault.address)).to.be.true + }) + }) + }) + + describe('registerVaultFactory', function () { + describe('as user', function () { + it('should fail', async function () { + await expect(geyser.connect(user).registerVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) + }) + }) + describe('when online', function () { + it('should succeed', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should update state', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + + expect(await geyser.getVaultFactorySetLength()).to.be.eq(1) + expect(await geyser.getVaultFactoryAtIndex(0)).to.be.eq(vaultFactory.address) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)) + .to.emit(geyser, 'VaultFactoryRegistered') + .withArgs(vaultFactory.address) + }) + }) + describe('when offline', function () { + beforeEach(async function () { + await powerSwitch.connect(admin).powerOff() + }) + it('should succeed', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should update state', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + + expect(await geyser.getVaultFactorySetLength()).to.be.eq(1) + expect(await geyser.getVaultFactoryAtIndex(0)).to.be.eq(vaultFactory.address) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)) + .to.emit(geyser, 'VaultFactoryRegistered') + .withArgs(vaultFactory.address) + }) + }) + describe('when shutdown', function () { + beforeEach(async function () { + await powerSwitch.connect(admin).emergencyShutdown() + }) + it('should fail', async function () { + await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Powered: is shutdown', + ) + }) + }) + describe('when already added', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should fail', async function () { + await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Geyser: vault factory already registered', + ) + }) + }) + describe('when removed', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + }) + it('should succeed', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should update state', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + + expect(await geyser.getVaultFactorySetLength()).to.be.eq(1) + expect(await geyser.getVaultFactoryAtIndex(0)).to.be.eq(vaultFactory.address) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)) + .to.emit(geyser, 'VaultFactoryRegistered') + .withArgs(vaultFactory.address) + }) + }) + describe('with second factory', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(admin.address) + }) + it('should succeed', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should update state', async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + + expect(await geyser.getVaultFactorySetLength()).to.be.eq(2) + expect(await geyser.getVaultFactoryAtIndex(1)).to.be.eq(vaultFactory.address) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)) + .to.emit(geyser, 'VaultFactoryRegistered') + .withArgs(vaultFactory.address) + }) + }) + }) + describe('removeVaultFactory', function () { + describe('as user', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should fail', async function () { + await expect(geyser.connect(user).removeVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) + }) + }) + describe('when online', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + }) + it('should succeed', async function () { + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + }) + it('should update state', async function () { + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + + expect(await geyser.getVaultFactorySetLength()).to.be.eq(0) + await expect(geyser.getVaultFactoryAtIndex(0)).to.be.reverted + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)) + .to.emit(geyser, 'VaultFactoryRemoved') + .withArgs(vaultFactory.address) + }) + }) + describe('when offline', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + await powerSwitch.connect(admin).powerOff() + }) + it('should succeed', async function () { + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + }) + it('should update state', async function () { + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + + expect(await geyser.getVaultFactorySetLength()).to.be.eq(0) + await expect(geyser.getVaultFactoryAtIndex(0)).to.be.reverted + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)) + .to.emit(geyser, 'VaultFactoryRemoved') + .withArgs(vaultFactory.address) + }) + }) + describe('when shutdown', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + await powerSwitch.connect(admin).emergencyShutdown() + }) + it('should fail', async function () { + await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Powered: is shutdown', + ) + }) + }) + describe('when never added', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Geyser: vault factory not registered', + ) + }) + }) + describe('when already removed', function () { + beforeEach(async function () { + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + }) + it('should fail', async function () { + await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)).to.be.revertedWith( + 'Geyser: vault factory not registered', + ) + }) + }) + }) + + describe('registerBonusToken', function () { + describe('as user', function () { + it('should fail', async function () { + await expect(geyser.connect(user).registerBonusToken(bonusToken.address)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) + }) + }) + describe('when online', function () { + describe('on first call', function () { + describe('with address zero', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).registerBonusToken(ethers.constants.AddressZero)).to.be.revertedWith( + 'Geyser: invalid address', + ) + }) + }) + describe('with geyser address', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).registerBonusToken(geyser.address)).to.be.revertedWith( + 'Geyser: invalid address', + ) + }) + }) + describe('with staking token', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).registerBonusToken(stakingToken.address)).to.be.revertedWith( + 'Geyser: invalid address', + ) + }) + }) + describe('with reward token', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).registerBonusToken(rewardToken.address)).to.be.revertedWith( + 'Geyser: invalid address', + ) + }) + }) + describe('with rewardPool address', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).registerBonusToken(rewardPool.address)).to.be.revertedWith( + 'Geyser: invalid address', + ) + }) + }) + describe('with bonus token', function () { + it('should succeed', async function () { + await geyser.connect(admin).registerBonusToken(bonusToken.address) + }) + it('should update state', async function () { + await geyser.connect(admin).registerBonusToken(bonusToken.address) + expect(await geyser.getBonusTokenSetLength()).to.eq(1) + expect(await geyser.getBonusTokenAtIndex(0)).to.eq(bonusToken.address) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).registerBonusToken(bonusToken.address)) + .to.emit(geyser, 'BonusTokenRegistered') + .withArgs(bonusToken.address) + }) + }) + }) + describe('on second call', function () { + beforeEach(async function () { + await geyser.connect(admin).registerBonusToken(bonusToken.address) + }) + describe('with same token', function () { + it('should fail', async function () { + await expect(geyser.connect(admin).registerBonusToken(bonusToken.address)).to.be.revertedWith( + 'Geyser: invalid address', + ) + }) + }) + describe('with different bonus token', function () { + let secondBonusToken: Contract + beforeEach(async function () { + secondBonusToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) + }) + it('should succeed', async function () { + await geyser.connect(admin).registerBonusToken(secondBonusToken.address) + }) + it('should update state', async function () { + await geyser.connect(admin).registerBonusToken(secondBonusToken.address) + expect(await geyser.getBonusTokenSetLength()).to.eq(2) + expect(await geyser.getBonusTokenAtIndex(0)).to.eq(bonusToken.address) + expect(await geyser.getBonusTokenAtIndex(1)).to.eq(secondBonusToken.address) + }) + it('should emit event', async function () { + await expect(geyser.connect(admin).registerBonusToken(secondBonusToken.address)) + .to.emit(geyser, 'BonusTokenRegistered') + .withArgs(secondBonusToken.address) + }) + }) + }) + }) + describe('when offline', function () { + it('should fail', async function () { + await powerSwitch.connect(admin).powerOff() + await expect(geyser.connect(admin).registerBonusToken(bonusToken.address)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('when shutdown', function () { + it('should fail', async function () { + await powerSwitch.connect(admin).emergencyShutdown() + await expect(geyser.connect(admin).registerBonusToken(bonusToken.address)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + }) + + describe('rescueTokensFromRewardPool', function () { + let otherToken: Contract + beforeEach(async function () { + otherToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) + await otherToken.connect(admin).transfer(rewardPool.address, mockTokenSupply) + await geyser.connect(admin).registerBonusToken(bonusToken.address) + }) + describe('as user', function () { + it('should fail', async function () { + await expect( + geyser.connect(user).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply), + ).to.be.revertedWith('Ownable: caller is not the owner') + }) + }) + describe('with reward token', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(rewardToken.address, admin.address, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with bonus token', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(bonusToken.address, admin.address, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with staking token', function () { + beforeEach(async function () { + await stakingToken.connect(admin).transfer(rewardPool.address, mockTokenSupply) + }) + it('should succeed', async function () { + await geyser.connect(admin).rescueTokensFromRewardPool(stakingToken.address, admin.address, mockTokenSupply) + }) + it('should transfer tokens', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(stakingToken.address, admin.address, mockTokenSupply), + ) + .to.emit(stakingToken, 'Transfer') + .withArgs(rewardPool.address, admin.address, mockTokenSupply) + }) + }) + describe('with geyser as recipient', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, geyser.address, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with staking token as recipient', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, stakingToken.address, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with reward token as recipient', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, rewardToken.address, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with rewardPool as recipient', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, rewardPool.address, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with address 0 as recipient', function () { + it('should fail', async function () { + await expect( + geyser + .connect(admin) + .rescueTokensFromRewardPool(otherToken.address, ethers.constants.AddressZero, mockTokenSupply), + ).to.be.revertedWith('Geyser: invalid address') + }) + }) + describe('with other address as recipient', function () { + it('should succeed', async function () { + await geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, user.address, mockTokenSupply) + }) + it('should transfer tokens', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, user.address, mockTokenSupply), + ) + .to.emit(otherToken, 'Transfer') + .withArgs(rewardPool.address, user.address, mockTokenSupply) + }) + }) + describe('with zero amount', function () { + it('should succeed', async function () { + await geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, 0) + }) + it('should transfer tokens', async function () { + await expect(geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, 0)) + .to.emit(otherToken, 'Transfer') + .withArgs(rewardPool.address, admin.address, 0) + }) + }) + describe('with partial amount', function () { + it('should succeed', async function () { + await geyser + .connect(admin) + .rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply.div(2)) + }) + it('should transfer tokens', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply.div(2)), + ) + .to.emit(otherToken, 'Transfer') + .withArgs(rewardPool.address, admin.address, mockTokenSupply.div(2)) + }) + }) + describe('with full amount', function () { + it('should succeed', async function () { + await geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply) + }) + it('should transfer tokens', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply), + ) + .to.emit(otherToken, 'Transfer') + .withArgs(rewardPool.address, admin.address, mockTokenSupply) + }) + }) + describe('with excess amount', function () { + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply.mul(2)), + ).to.be.revertedWith('') + }) + }) + describe('when online', function () { + it('should succeed', async function () { + await geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply) + }) + it('should transfer tokens', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply), + ) + .to.emit(otherToken, 'Transfer') + .withArgs(rewardPool.address, admin.address, mockTokenSupply) + }) + }) + describe('when offline', function () { + beforeEach(async function () { + await powerSwitch.connect(admin).powerOff() + }) + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply), + ).to.be.revertedWith('Powered: is not online') + }) + }) + describe('when shutdown', function () { + beforeEach(async function () { + await powerSwitch.connect(admin).emergencyShutdown() + }) + it('should fail', async function () { + await expect( + geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, admin.address, mockTokenSupply), + ).to.be.revertedWith('Powered: is not online') + }) + }) + }) + }) + + describe('user functions', function () { + let geyser: Contract, powerSwitch: Contract, rewardPool: Contract + beforeEach(async function () { + const args = [ + admin.address, + rewardPoolFactory.address, + powerSwitchFactory.address, + stakingToken.address, + rewardToken.address, + [rewardScaling.floor, rewardScaling.ceiling, rewardScaling.time], + ] + geyser = await deployAludel(args) + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + powerSwitch = await ethers.getContractAt('PowerSwitch', await geyser.getPowerSwitch()) + rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getGeyserData()).rewardPool) + }) + + describe('stake', function () { + const stakeAmount = mockTokenSupply.div(100) + let vault: Contract + + beforeEach(async function () { + vault = await createInstance('UniversalVault', vaultFactory, user) + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + }) + describe('when offline', function () { + it('should fail', async function () { + await powerSwitch.connect(admin).powerOff() + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('when shutdown', function () { + it('should fail', async function () { + await powerSwitch.connect(admin).emergencyShutdown() + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('to invalid vault', function () { + it('should fail', async function () { + await geyser.connect(admin).removeVaultFactory(vaultFactory.address) + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)).to.be.revertedWith( + 'Geyser: vault is not registered', + ) + }) + }) + describe('with amount of zero', function () { + it('should fail', async function () { + await expect(stake(user, geyser, vault, stakingToken, '0')).to.be.revertedWith('Geyser: no amount staked') + }) + }) + describe('with insufficient balance', function () { + it('should fail', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount.mul(2))).to.be.revertedWith( + 'UniversalVault: insufficient balance', + ) + }) + }) + describe('when not funded', function () { + it('should succeed', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + }) + }) + describe('when funded', function () { + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) + await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + }) + describe('on first stake', function () { + describe('as vault owner', function () { + it('should succeed', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.totalStake).to.eq(stakeAmount) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + + expect(vaultData.totalStake).to.eq(stakeAmount) + expect(vaultData.stakes.length).to.eq(1) + expect(vaultData.stakes[0].amount).to.eq(stakeAmount) + expect(vaultData.stakes[0].timestamp).to.eq(await getTimestamp()) + }) + it('should emit event', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(geyser, 'Staked') + .withArgs(vault.address, stakeAmount) + }) + it('should lock tokens', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Locked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + }) + describe('on second stake', function () { + beforeEach(async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount.div(2)) + }) + it('should succeed', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount.div(2)) + }) + it('should update state', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount.div(2)) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.totalStake).to.eq(stakeAmount) + expect(geyserData.totalStakeUnits).to.eq(stakeAmount.div(2)) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + + expect(vaultData.totalStake).to.eq(stakeAmount) + expect(vaultData.stakes.length).to.eq(2) + expect(vaultData.stakes[0].amount).to.eq(stakeAmount.div(2)) + expect(vaultData.stakes[0].timestamp).to.eq((await getTimestamp()) - 1) + expect(vaultData.stakes[1].amount).to.eq(stakeAmount.div(2)) + expect(vaultData.stakes[1].timestamp).to.eq(await getTimestamp()) + }) + it('should emit event', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount.div(2))) + .to.emit(geyser, 'Staked') + .withArgs(vault.address, stakeAmount.div(2)) + }) + it('should lock tokens', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount.div(2))) + .to.emit(vault, 'Locked') + .withArgs(geyser.address, stakingToken.address, stakeAmount.div(2)) + }) + }) + describe('when MAX_STAKES_PER_VAULT reached', function () { + let quantity: number + beforeEach(async function () { + quantity = (await geyser.MAX_STAKES_PER_VAULT()).toNumber() + for (let index = 0; index < quantity; index++) { + await stake(user, geyser, vault, stakingToken, stakeAmount.div(quantity)) + } + }) + it('should fail', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount.div(quantity))).to.be.revertedWith( + 'Geyser: MAX_STAKES_PER_VAULT reached', + ) + }) + }) + }) + describe('when stakes reset', function () { + beforeEach(async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should succeed', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.totalStake).to.eq(stakeAmount) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + + expect(vaultData.totalStake).to.eq(stakeAmount) + expect(vaultData.stakes.length).to.eq(1) + expect(vaultData.stakes[0].amount).to.eq(stakeAmount) + expect(vaultData.stakes[0].timestamp).to.eq(await getTimestamp()) + }) + it('should emit event', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(geyser, 'Staked') + .withArgs(vault.address, stakeAmount) + }) + it('should lock tokens', async function () { + await expect(stake(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Locked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + }) + + describe('unstake', function () { + const stakeAmount = ethers.utils.parseEther('100') + const rewardAmount = ethers.utils.parseUnits('1000', 9) + + describe('with default config', function () { + let vault: Contract + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + }) + describe('when offline', function () { + it('should fail', async function () { + await powerSwitch.connect(admin).powerOff() + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('when shutdown', function () { + it('should fail', async function () { + await powerSwitch.connect(admin).emergencyShutdown() + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + }) + describe('with invalid vault', function () { + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + }) + describe('with permissioned not signed by owner', function () { + it('should fail', async function () { + await expect( + unstakeAndClaim(Wallet.createRandom().connect(ethers.provider), geyser, vault, stakingToken, stakeAmount), + ).to.be.revertedWith('ERC1271: Invalid signature') + }) + }) + describe('with amount of zero', function () { + it('should fail', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, 0)).to.be.revertedWith( + 'Geyser: no amount unstaked', + ) + }) + }) + describe('with amount greater than stakes', function () { + it('should fail', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.add(1))).to.be.revertedWith( + 'Geyser: insufficient vault stake', + ) + }) + }) + }) + describe('with fully vested stake', function () { + let vault: Contract + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(0) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, rewardAmount) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, rewardAmount) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with partially vested stake', function () { + const stakeDuration = rewardScaling.time / 2 + const expectedReward = calculateExpectedReward(stakeAmount, stakeDuration, rewardAmount, 0) + + let vault: Contract + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(stakeDuration) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with floor and ceiling scaled up', function () { + const stakeDuration = rewardScaling.time / 2 + const expectedReward = calculateExpectedReward(stakeAmount, stakeDuration, rewardAmount, 0) + + let vault: Contract + beforeEach(async function () { + const args = [ + admin.address, + rewardPoolFactory.address, + powerSwitchFactory.address, + stakingToken.address, + rewardToken.address, + + [rewardScaling.floor * 2, rewardScaling.ceiling * 2, rewardScaling.time], + ] + geyser = await deployAludel(args) + await geyser.connect(admin).registerVaultFactory(vaultFactory.address) + powerSwitch = await ethers.getContractAt('PowerSwitch', await geyser.getPowerSwitch()) + rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getGeyserData()).rewardPool) + + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(stakeDuration) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with no reward', function () { + let vault: Contract + beforeEach(async function () { + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(0) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with partially vested reward', function () { + const expectedReward = calculateExpectedReward(stakeAmount, rewardScaling.time, rewardAmount.div(2), 0) + + let vault: Contract + beforeEach(async function () { + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time / 2) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with flash stake', function () { + let vault: Contract, MockStakeHelper: Contract + + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + MockStakeHelper = await deployContract('MockStakeHelper') + }) + it('should succeed', async function () { + await MockStakeHelper.flashStake( + geyser.address, + vault.address, + stakeAmount, + await signPermission('Lock', vault, user, geyser.address, stakingToken.address, stakeAmount), + await signPermission( + 'Unlock', + vault, + user, + geyser.address, + stakingToken.address, + stakeAmount, + (await vault.getNonce()).add(1), + ), + ) + }) + it('should update state', async function () { + await MockStakeHelper.flashStake( + geyser.address, + vault.address, + stakeAmount, + await signPermission('Lock', vault, user, geyser.address, stakingToken.address, stakeAmount), + await signPermission( + 'Unlock', + vault, + user, + geyser.address, + stakingToken.address, + stakeAmount, + (await vault.getNonce()).add(1), + ), + ) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = MockStakeHelper.flashStake( + geyser.address, + vault.address, + stakeAmount, + await signPermission('Lock', vault, user, geyser.address, stakingToken.address, stakeAmount), + await signPermission( + 'Unlock', + vault, + user, + geyser.address, + stakingToken.address, + stakeAmount, + (await vault.getNonce()).add(1), + ), + ) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + }) + it('should lock tokens', async function () { + await expect( + MockStakeHelper.flashStake( + geyser.address, + vault.address, + stakeAmount, + await signPermission('Lock', vault, user, geyser.address, stakingToken.address, stakeAmount), + await signPermission( + 'Unlock', + vault, + user, + geyser.address, + stakingToken.address, + stakeAmount, + (await vault.getNonce()).add(1), + ), + ), + ) + .to.emit(vault, 'Locked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + it('should unlock tokens', async function () { + await expect( + MockStakeHelper.flashStake( + geyser.address, + vault.address, + stakeAmount, + await signPermission('Lock', vault, user, geyser.address, stakingToken.address, stakeAmount), + await signPermission( + 'Unlock', + vault, + user, + geyser.address, + stakingToken.address, + stakeAmount, + (await vault.getNonce()).add(1), + ), + ), + ) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with one second stake', function () { + const stakeDuration = 1 + const expectedReward = calculateExpectedReward(stakeAmount, stakeDuration, rewardAmount, 0) + + let vault: Contract + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await network.provider.request({ + method: 'evm_increaseTime', + params: [1], + }) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with partial amount from single stake', function () { + const expectedReward = calculateExpectedReward( + stakeAmount.div(2), + rewardScaling.time, + rewardAmount, + stakeAmount.div(2).mul(rewardScaling.time), + ) + + let vault: Contract + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.div(2)) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.div(2)) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(stakeAmount.div(2)) + expect(geyserData.totalStakeUnits).to.eq(stakeAmount.div(2).mul(rewardScaling.time)) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(stakeAmount.div(2)) + expect(vaultData.stakes.length).to.eq(1) + expect(vaultData.stakes[0].amount).to.eq(stakeAmount.div(2)) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.div(2)) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount.div(2)) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.div(2))) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.div(2))) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount.div(2)) + }) + }) + describe('with partial amount from multiple stakes', function () { + const currentStake = ethers.utils.parseEther('99') + const unstakedAmount = currentStake.div(2) + const expectedReward = calculateExpectedReward( + unstakedAmount, + rewardScaling.time, + rewardAmount, + currentStake.div(2).mul(rewardScaling.time), + ).sub(1) // account for division dust + const quantity = 3 + + let vault: Contract + beforeEach(async function () { + // fund geyser + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + // deploy vault and transfer stake + vault = await createInstance('UniversalVault', vaultFactory, user) + await stakingToken.connect(admin).transfer(vault.address, currentStake) + + // perform multiple stakes in same block + const permissions = [] + for (let index = 0; index < quantity; index++) { + permissions.push( + await signPermission( + 'Lock', + vault, + user, + geyser.address, + stakingToken.address, + currentStake.div(quantity), + index, + ), + ) + } + const MockStakeHelper = await deployContract('MockStakeHelper') + await MockStakeHelper.stakeBatch( + new Array(quantity).fill(undefined).map(() => geyser.address), + new Array(quantity).fill(undefined).map(() => vault.address), + new Array(quantity).fill(undefined).map(() => currentStake.div(quantity)), + permissions, + ) + + // increase time to the end of reward scaling + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(currentStake.sub(unstakedAmount)) + expect(geyserData.totalStakeUnits).to.eq(currentStake.sub(unstakedAmount).mul(rewardScaling.time)) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(currentStake.sub(unstakedAmount)) + expect(vaultData.stakes.length).to.eq(2) + expect(vaultData.stakes[0].amount).to.eq(currentStake.div(3)) + expect(vaultData.stakes[1].amount).to.eq(currentStake.div(6)) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, unstakedAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, unstakedAmount) + }) + }) + describe('with full amount of the last of multiple stakes', function () { + const currentStake = ethers.utils.parseEther('99') + const unstakedAmount = currentStake.div(3) + const expectedReward = calculateExpectedReward( + unstakedAmount, + rewardScaling.time, + rewardAmount, + currentStake.sub(unstakedAmount).mul(rewardScaling.time), + ) + + const quantity = 3 + + let vault: Contract + beforeEach(async function () { + // fund geyser + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + // deploy vault and transfer stake + vault = await createInstance('UniversalVault', vaultFactory, user) + await stakingToken.connect(admin).transfer(vault.address, currentStake) + + // perform multiple stakes in same block + const permissions = [] + for (let index = 0; index < quantity; index++) { + permissions.push( + await signPermission( + 'Lock', + vault, + user, + geyser.address, + stakingToken.address, + currentStake.div(quantity), + index, + ), + ) + } + const MockStakeHelper = await deployContract('MockStakeHelper') + await MockStakeHelper.stakeBatch( + new Array(quantity).fill(geyser.address), + new Array(quantity).fill(vault.address), + new Array(quantity).fill(currentStake.div(quantity)), + permissions, + ) + + // increase time to the end of reward scaling + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(currentStake.sub(unstakedAmount)) + expect(geyserData.totalStakeUnits).to.eq(currentStake.sub(unstakedAmount).mul(rewardScaling.time)) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(currentStake.sub(unstakedAmount)) + expect(vaultData.stakes.length).to.eq(2) + expect(vaultData.stakes[0].amount).to.eq(currentStake.div(3)) + expect(vaultData.stakes[1].amount).to.eq(currentStake.div(3)) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, unstakedAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, unstakedAmount) + }) + }) + describe('with full amount of multiple stakes', function () { + const currentStake = ethers.utils.parseEther('99') + const unstakedAmount = currentStake + const expectedReward = calculateExpectedReward(unstakedAmount, rewardScaling.time, rewardAmount, 0) + const quantity = 3 + + let vault: Contract + beforeEach(async function () { + // fund geyser + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + // deploy vault and transfer stake + vault = await createInstance('UniversalVault', vaultFactory, user) + await stakingToken.connect(admin).transfer(vault.address, currentStake) + + // perform multiple stakes in same block + const permissions = [] + for (let index = 0; index < quantity; index++) { + permissions.push( + await signPermission( + 'Lock', + vault, + user, + geyser.address, + stakingToken.address, + currentStake.div(quantity), + index, + ), + ) + } + const MockStakeHelper = await deployContract('MockStakeHelper') + await MockStakeHelper.stakeBatch( + new Array(quantity).fill(geyser.address), + new Array(quantity).fill(vault.address), + new Array(quantity).fill(currentStake.div(quantity)), + permissions, + ) + + // increase time to the end of reward scaling + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(0) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, unstakedAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, expectedReward) + }) + it('should transfer tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, unstakedAmount) + }) + }) + describe('when one bonus token', function () { + let vault: Contract + beforeEach(async function () { + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + await geyser.connect(admin).registerBonusToken(bonusToken.address) + + vault = await createInstance('UniversalVault', vaultFactory, user) + + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + }) + describe('with no bonus token balance', function () { + beforeEach(async function () { + await stake(user, geyser, vault, stakingToken, stakeAmount) + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(0) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, rewardAmount) + }) + it('should transfer tokens', async function () { + const txPromise = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(txPromise) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, rewardAmount) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with fully vested stake', function () { + beforeEach(async function () { + await bonusToken.connect(admin).transfer(rewardPool.address, mockTokenSupply) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(0) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, rewardToken.address, rewardAmount) + await expect(tx) + .to.emit(geyser, 'RewardClaimed') + .withArgs(vault.address, bonusToken.address, mockTokenSupply) + }) + it('should transfer tokens', async function () { + const txPromise = unstakeAndClaim( + user, + + geyser, + vault, + stakingToken, + stakeAmount, + ) + await expect(txPromise) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, rewardAmount) + await expect(txPromise) + .to.emit(bonusToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, mockTokenSupply) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + describe('with partially vested stake', function () { + const stakeDuration = rewardScaling.time / 2 + const expectedReward = calculateExpectedReward(stakeAmount, stakeDuration, rewardAmount, 0) + const expectedBonus = calculateExpectedReward(stakeAmount, stakeDuration, mockTokenSupply, 0) + beforeEach(async function () { + await bonusToken.connect(admin).transfer(rewardPool.address, mockTokenSupply) + + await stake(user, geyser, vault, stakingToken, stakeAmount) + + await increaseTime(stakeDuration) + }) + it('should succeed', async function () { + await unstakeAndClaim( + user, + + geyser, + vault, + stakingToken, + stakeAmount, + ) + }) + it('should update state', async function () { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + it('should emit event', async function () { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx) + .to.emit(geyser, 'RewardClaimed') + .withArgs(vault.address, rewardToken.address, expectedReward) + await expect(tx).to.emit(geyser, 'RewardClaimed').withArgs(vault.address, bonusToken.address, expectedBonus) + }) + it('should transfer tokens', async function () { + const txPromise = unstakeAndClaim( + user, + + geyser, + vault, + stakingToken, + stakeAmount, + ) + await expect(txPromise) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedReward) + await expect(txPromise) + .to.emit(bonusToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, expectedBonus) + }) + it('should unlock tokens', async function () { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + }) + }) + }) + describe('with multiple vaults', function () { + const stakeAmount = ethers.utils.parseEther('1') + const rewardAmount = ethers.utils.parseUnits('1000', 9) + const quantity = 10 + + let vaults: Array + beforeEach(async function () { + // fund geyser + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + await increaseTime(rewardScaling.time) + + // create vaults + vaults = [] + const permissions = [] + for (let index = 0; index < quantity; index++) { + const vault = await createInstance('UniversalVault', vaultFactory, user) + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + + vaults.push(vault) + + permissions.push( + await signPermission('Lock', vault, user, geyser.address, stakingToken.address, stakeAmount), + ) + } + + // stake in same block + const MockStakeHelper = await deployContract('MockStakeHelper') + await MockStakeHelper.stakeBatch( + new Array(quantity).fill(geyser.address), + vaults.map((vault) => vault.address), + new Array(quantity).fill(stakeAmount), + permissions, + ) + + // increase time to end of reward scaling + await increaseTime(rewardScaling.time) + }) + it('should succeed', async function () { + for (const vault of vaults) { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + } + }) + it('should update state', async function () { + for (const vault of vaults) { + await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + } + + const geyserData = await geyser.getGeyserData() + + expect(geyserData.rewardSharesOutstanding).to.eq(0) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + }) + it('should emit event', async function () { + for (const vault of vaults) { + const tx = unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) + await expect(tx).to.emit(geyser, 'Unstaked').withArgs(vault.address, stakeAmount) + await expect(tx) + .to.emit(geyser, 'RewardClaimed') + .withArgs(vault.address, rewardToken.address, rewardAmount.div(quantity)) + } + }) + it('should transfer tokens', async function () { + for (const vault of vaults) { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(rewardToken, 'Transfer') + .withArgs(rewardPool.address, vault.address, rewardAmount.div(quantity)) + } + }) + it('should unlock tokens', async function () { + for (const vault of vaults) { + await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount)) + .to.emit(vault, 'Unlocked') + .withArgs(geyser.address, stakingToken.address, stakeAmount) + } + }) + }) + }) + + describe('rageQuit', function () { + const stakeAmount = ethers.utils.parseEther('100') + const rewardAmount = ethers.utils.parseUnits('1000', 9) + const gasLimit = 600_000 + + let vault: Contract + beforeEach(async function () { + // fund geyser + await rewardToken.connect(admin).approve(geyser.address, rewardAmount) + await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + + // create vault + vault = await createInstance('UniversalVault', vaultFactory, user) + + // stake + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + await stake(user, geyser, vault, stakingToken, stakeAmount) + }) + describe('when online', function () { + it('should succeed', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + }) + it('should update state', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + }) + describe('when offline', function () { + beforeEach(async function () { + await powerSwitch.connect(admin).powerOff() + }) + it('should succeed', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + }) + it('should update state', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + }) + describe('when shutdown', function () { + beforeEach(async function () { + await powerSwitch.connect(admin).emergencyShutdown() + }) + it('should succeed', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + }) + it('should update state', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + }) + describe('with unknown vault', function () { + it('should fail', async function () { + await expect( + geyser.connect(user).rageQuit({ + gasLimit, + }), + ).to.be.revertedWith('Geyser: no stake') + }) + }) + describe('when no stake', function () { + it('should fail', async function () { + const secondVault = await createInstance('UniversalVault', vaultFactory, user) + await expect( + secondVault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }), + ).to.be.revertedWith('UniversalVault: missing lock') + }) + }) + describe('when insufficient gas', function () { + it('should fail', async function () { + await expect( + vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit: await vault.RAGEQUIT_GAS(), + }), + ).to.be.revertedWith('UniversalVault: insufficient gas') + }) + }) + describe('when insufficient gas with multiple stakes', function () { + let quantity: number + beforeEach(async function () { + quantity = (await geyser.MAX_STAKES_PER_VAULT()).toNumber() - 1 + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + for (let index = 0; index < quantity; index++) { + await stake(user, geyser, vault, stakingToken, stakeAmount.div(quantity)) + } + }) + it('should fail', async function () { + await expect( + vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit: await vault.RAGEQUIT_GAS(), + }), + ).to.be.revertedWith('UniversalVault: insufficient gas') + }) + }) + describe('when single stake', function () { + it('should succeed', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + }) + it('should update state', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + }) + describe('when multiple stakes', function () { + let quantity: number + + beforeEach(async function () { + quantity = (await geyser.MAX_STAKES_PER_VAULT()).toNumber() - 1 + await stakingToken.connect(admin).transfer(vault.address, stakeAmount) + for (let index = 0; index < quantity; index++) { + await stake(user, geyser, vault, stakingToken, stakeAmount.div(quantity)) + } + }) + it('should succeed', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + }) + it('should update state', async function () { + await vault.connect(user).rageQuit(geyser.address, stakingToken.address, { + gasLimit, + }) + + const geyserData = await geyser.getGeyserData() + const vaultData = await geyser.getVaultData(vault.address) + + expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) + expect(geyserData.totalStake).to.eq(0) + expect(geyserData.totalStakeUnits).to.eq(0) + expect(geyserData.lastUpdate).to.eq(await getTimestamp()) + expect(vaultData.totalStake).to.eq(0) + expect(vaultData.stakes.length).to.eq(0) + }) + }) + }) + }) +}) diff --git a/test/PowerSwitch/PowerSwitch.ts b/test/PowerSwitch/PowerSwitch.ts new file mode 100644 index 0000000..cd85e58 --- /dev/null +++ b/test/PowerSwitch/PowerSwitch.ts @@ -0,0 +1,103 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { ethers } from 'hardhat' +import { deployContract } from '../utils' + +describe('PowerSwitch', function () { + let accounts: SignerWithAddress[] + let Mock: Contract + + beforeEach(async function () { + // prepare signers + accounts = await ethers.getSigners() + // deploy mock + Mock = await deployContract('PowerSwitch', [accounts[0].address]) + }) + + describe('powerOn', function () { + it('should fail if msg.sender is not admin', async function () { + expect(await Mock.getPowerController()).to.eq(accounts[0].address) + await Mock.connect(accounts[0]).powerOff() + await expect(Mock.connect(accounts[1]).powerOn()).to.be.revertedWith('Ownable: caller is not the owner') + }) + it('should succeed if in offline state', async function () { + await Mock.connect(accounts[0]).powerOff() + await Mock.connect(accounts[0]).powerOn() + }) + it('should fail if in online state', async function () { + await expect(Mock.connect(accounts[0]).powerOn()).to.be.revertedWith('PowerSwitch: cannot power on') + }) + it('should fail if in shutdown state', async function () { + await Mock.connect(accounts[0]).emergencyShutdown() + await expect(Mock.connect(accounts[0]).powerOn()).to.be.revertedWith('PowerSwitch: cannot power on') + }) + it('should succeed and emit event', async function () { + await Mock.connect(accounts[0]).powerOff() + const txPromise = Mock.connect(accounts[0]).powerOn() + // validate event + await expect(txPromise).to.emit(Mock, 'PowerOn') + // validate state + expect(await Mock.isOnline()).to.eq(true) + expect(await Mock.isOffline()).to.eq(false) + expect(await Mock.isShutdown()).to.eq(false) + expect(await Mock.getStatus()).to.eq(0) + }) + }) + + describe('powerOff', function () { + it('should fail if msg.sender is not admin', async function () { + expect(await Mock.getPowerController()).to.eq(accounts[0].address) + await expect(Mock.connect(accounts[1]).powerOff()).to.be.revertedWith('Ownable: caller is not the owner') + }) + it('should succeed if in online state', async function () { + await Mock.connect(accounts[0]).powerOff() + }) + it('should fail if in offline state', async function () { + await Mock.connect(accounts[0]).powerOff() + await expect(Mock.connect(accounts[0]).powerOff()).to.be.revertedWith('PowerSwitch: cannot power off') + }) + it('should fail if in shutdown state', async function () { + await Mock.connect(accounts[0]).emergencyShutdown() + await expect(Mock.connect(accounts[0]).powerOff()).to.be.revertedWith('PowerSwitch: cannot power off') + }) + it('should succeed and emit event', async function () { + const txPromise = Mock.connect(accounts[0]).powerOff() + // validate event + await expect(txPromise).to.emit(Mock, 'PowerOff') + // validate state + expect(await Mock.isOnline()).to.eq(false) + expect(await Mock.isOffline()).to.eq(true) + expect(await Mock.isShutdown()).to.eq(false) + expect(await Mock.getStatus()).to.eq(1) + }) + }) + + describe('emergencyShutdown', function () { + it('should fail if msg.sender is not admin', async function () { + expect(await Mock.getPowerController()).to.eq(accounts[0].address) + await expect(Mock.connect(accounts[1]).emergencyShutdown()).to.be.revertedWith('Ownable: caller is not the owner') + }) + it('should succeed if in online state', async function () { + await Mock.connect(accounts[0]).emergencyShutdown() + }) + it('should succeed if in offline state', async function () { + await Mock.connect(accounts[0]).powerOff() + await Mock.connect(accounts[0]).emergencyShutdown() + }) + it('should fail if in shutdown state', async function () { + await Mock.connect(accounts[0]).emergencyShutdown() + await expect(Mock.connect(accounts[0]).emergencyShutdown()).to.be.revertedWith('PowerSwitch: cannot shutdown') + }) + it('should succeed and emit event', async function () { + const txPromise = Mock.connect(accounts[0]).emergencyShutdown() + // validate event + await expect(txPromise).to.emit(Mock, 'EmergencyShutdown') + // validate state + expect(await Mock.isOnline()).to.eq(false) + expect(await Mock.isOffline()).to.eq(false) + expect(await Mock.isShutdown()).to.eq(true) + expect(await Mock.getStatus()).to.eq(2) + }) + }) +}) diff --git a/test/PowerSwitch/Powered.ts b/test/PowerSwitch/Powered.ts new file mode 100644 index 0000000..42b7f4c --- /dev/null +++ b/test/PowerSwitch/Powered.ts @@ -0,0 +1,99 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { ethers } from 'hardhat' +import { deployContract } from '../utils' + +describe('Powered', function () { + let accounts: SignerWithAddress[] + let PowerSwitch: Contract + let Mock: Contract + + beforeEach(async function () { + // prepare signers + accounts = await ethers.getSigners() + // deploy mock + PowerSwitch = await deployContract('PowerSwitch', [accounts[0].address]) + Mock = await deployContract('MockPowered', [PowerSwitch.address]) + }) + + describe('getPowerSwitch', function () { + it('should succeed', async function () { + expect(await Mock.getPowerSwitch()).to.eq(PowerSwitch.address) + }) + }) + + describe('getPowerController', function () { + it('should succeed', async function () { + expect(await Mock.getPowerController()).to.eq(accounts[0].address) + }) + }) + + describe('onlyOnline', function () { + it('should succeed if online', async function () { + expect(await Mock.isOnline()).to.eq(true) + await Mock.onlyOnlineCall() + }) + it('should fail if offline', async function () { + await PowerSwitch.connect(accounts[0]).powerOff() + expect(await Mock.isOffline()).to.eq(true) + await expect(Mock.onlyOnlineCall()).to.be.revertedWith('Powered: is not online') + }) + it('should fail if shutdown', async function () { + await PowerSwitch.connect(accounts[0]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await expect(Mock.onlyOnlineCall()).to.be.revertedWith('Powered: is not online') + }) + }) + + describe('onlyOffline', function () { + it('should fail if online', async function () { + expect(await Mock.isOnline()).to.eq(true) + await expect(Mock.onlyOfflineCall()).to.be.revertedWith('Powered: is not offline') + }) + it('should succeed if offline', async function () { + await PowerSwitch.connect(accounts[0]).powerOff() + expect(await Mock.isOffline()).to.eq(true) + await Mock.onlyOfflineCall() + }) + it('should fail if shutdown', async function () { + await PowerSwitch.connect(accounts[0]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await expect(Mock.onlyOfflineCall()).to.be.revertedWith('Powered: is not offline') + }) + }) + + describe('notShutdown', function () { + it('should succeed if online', async function () { + expect(await Mock.isOnline()).to.eq(true) + await Mock.notShutdownCall() + }) + it('should succeed if offline', async function () { + await PowerSwitch.connect(accounts[0]).powerOff() + expect(await Mock.isOffline()).to.eq(true) + await Mock.notShutdownCall() + }) + it('should fail if shutdown', async function () { + await PowerSwitch.connect(accounts[0]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await expect(Mock.notShutdownCall()).to.be.revertedWith('Powered: is shutdown') + }) + }) + + describe('onlyShutdown', function () { + it('should fail if online', async function () { + expect(await Mock.isOnline()).to.eq(true) + await expect(Mock.onlyShutdownCall()).to.be.revertedWith('Powered: is not shutdown') + }) + it('should fail if offline', async function () { + await PowerSwitch.connect(accounts[0]).powerOff() + expect(await Mock.isOffline()).to.eq(true) + await expect(Mock.onlyShutdownCall()).to.be.revertedWith('Powered: is not shutdown') + }) + it('should succeed if shutdown', async function () { + await PowerSwitch.connect(accounts[0]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await Mock.onlyShutdownCall() + }) + }) +}) diff --git a/test/RewardPool.ts b/test/RewardPool.ts new file mode 100644 index 0000000..1207998 --- /dev/null +++ b/test/RewardPool.ts @@ -0,0 +1,124 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { ethers } from 'hardhat' +import { deployContract } from './utils' + +describe('RewardPool', function () { + let accounts: SignerWithAddress[] + let PowerSwitch: Contract + let ERC20: Contract + let Mock: Contract + const amount = ethers.utils.parseEther('10') + + beforeEach(async function () { + // prepare signers + accounts = await ethers.getSigners() + // deploy mock + PowerSwitch = await deployContract('PowerSwitch', [accounts[1].address]) + Mock = await deployContract('RewardPool', [PowerSwitch.address]) + ERC20 = await deployContract('MockERC20', [Mock.address, amount]) + }) + + describe('sendERC20', function () { + it('should succeed if msg.sender is admin', async function () { + await Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, amount) + }) + it('should fail if msg.sender is controller', async function () { + await expect(Mock.connect(accounts[1]).sendERC20(ERC20.address, accounts[0].address, amount)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) + }) + it('should succeed if online', async function () { + expect(await Mock.isOnline()).to.eq(true) + await Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, amount) + }) + it('should fail if offline', async function () { + await PowerSwitch.connect(accounts[1]).powerOff() + expect(await Mock.isOffline()).to.eq(true) + await expect(Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, amount)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + it('should fail if shutdown', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await expect(Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, amount)).to.be.revertedWith( + 'Powered: is not online', + ) + }) + it('should succeed with full balance', async function () { + let txPromise = Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, amount) + await expect(txPromise).to.emit(ERC20, 'Transfer').withArgs(Mock.address, accounts[0].address, amount) + }) + it('should succeed with partial balance', async function () { + let txPromise = Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, amount.div(2)) + await expect(txPromise).to.emit(ERC20, 'Transfer').withArgs(Mock.address, accounts[0].address, amount.div(2)) + }) + it('should succeed with no balance', async function () { + let txPromise = Mock.connect(accounts[0]).sendERC20(ERC20.address, accounts[0].address, '0') + await expect(txPromise).to.emit(ERC20, 'Transfer').withArgs(Mock.address, accounts[0].address, '0') + }) + }) + + describe('rescueERC20', function () { + it('should fail if msg.sender is admin', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + await expect(Mock.connect(accounts[0]).rescueERC20([ERC20.address], accounts[0].address)).to.be.revertedWith( + 'RewardPool: only controller can withdraw after shutdown', + ) + }) + it('should succeed if msg.sender is controller', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + await Mock.connect(accounts[1]).rescueERC20([ERC20.address], accounts[0].address) + }) + it('should fail if online', async function () { + expect(await Mock.isOnline()).to.eq(true) + expect(await PowerSwitch.isOnline()).to.eq(true) + await expect(Mock.connect(accounts[1]).rescueERC20([ERC20.address], accounts[0].address)).to.be.revertedWith( + 'Powered: is not shutdown', + ) + }) + it('should fail if offline', async function () { + await PowerSwitch.connect(accounts[1]).powerOff() + expect(await Mock.isOffline()).to.eq(true) + await expect(Mock.connect(accounts[1]).rescueERC20([ERC20.address], accounts[0].address)).to.be.revertedWith( + 'Powered: is not shutdown', + ) + }) + it('should succeed if shutdown', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await Mock.connect(accounts[1]).rescueERC20([ERC20.address], accounts[0].address) + }) + it('should fail if recipient is not defined', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + await expect( + Mock.connect(accounts[1]).rescueERC20([ERC20.address], ethers.constants.AddressZero), + ).to.be.revertedWith('RewardPool: recipient not defined') + }) + it('should succeed with single token', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + let txPromise = Mock.connect(accounts[1]).rescueERC20([ERC20.address], accounts[0].address) + await expect(txPromise).to.emit(ERC20, 'Transfer').withArgs(Mock.address, accounts[0].address, amount) + }) + it('should succeed with 100 tokens', async function () { + await PowerSwitch.connect(accounts[1]).emergencyShutdown() + expect(await Mock.isShutdown()).to.eq(true) + let num = 100 + let tokens = [] + for (let index = 0; index < num; index++) { + tokens.push(await deployContract('MockERC20', [Mock.address, amount])) + } + let txPromise = Mock.connect(accounts[1]).rescueERC20( + tokens.map((token) => token.address), + accounts[0].address, + ) + for (let index = 0; index < num; index++) { + await expect(txPromise).to.emit(tokens[index], 'Transfer').withArgs(Mock.address, accounts[0].address, amount) + } + }) + }) +}) diff --git a/test/UniversalVault.ts b/test/UniversalVault.ts new file mode 100644 index 0000000..ddf8c96 --- /dev/null +++ b/test/UniversalVault.ts @@ -0,0 +1,609 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract, Wallet } from 'ethers' +import { ethers } from 'hardhat' +import { createInstance, deployContract, ETHER, signPermission } from './utils' + +enum DelegateType { + Succeed, + Revert, + RevertWithMessage, + OOG, +} + +describe('UniversalVault', function () { + let accounts: SignerWithAddress[], admin: SignerWithAddress, recipient: SignerWithAddress, delegate: SignerWithAddress + let owner: Wallet + let factory: Contract, vault: Contract + + before(async function () { + // prepare signers + accounts = await ethers.getSigners() + admin = accounts[0] + recipient = accounts[1] + delegate = accounts[2] + owner = Wallet.createRandom().connect(ethers.provider) + await delegate.sendTransaction({ + to: owner.address, + value: (await delegate.getBalance()).mul(9).div(10), + }) + }) + + beforeEach(async function () { + // deploy template + const template = await deployContract('UniversalVault') + + // deploy factory + factory = await deployContract('VaultFactory', [template.address]) + + // deploy instance + vault = await createInstance('UniversalVault', factory, owner) + }) + + describe('nft', function () { + it('should succeed', async function () { + expect(await vault.nft()).to.eq(factory.address) + }) + }) + + describe('getNonce', function () { + it('should succeed', async function () { + expect(await vault.getNonce()).to.eq(0) + }) + }) + + describe('owner', function () { + it('should succeed', async function () { + expect(await vault.owner()).to.eq(owner.address) + }) + }) + + describe('getLockSetCount', function () { + it('should succeed', async function () { + expect(await vault.getLockSetCount()).to.eq(0) + }) + }) + + describe('getLockAt', function () { + it('should fail when no locks', async function () { + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + }) + }) + + describe('getBalanceDelegated', function () { + it('should succeed', async function () { + expect(await vault.getBalanceDelegated(ethers.constants.AddressZero, ethers.constants.AddressZero)).to.deep.eq(0) + }) + }) + + describe('getBalanceLocked', function () { + it('should succeed', async function () { + expect(await vault.getBalanceLocked(ethers.constants.AddressZero)).to.deep.eq(0) + }) + }) + + describe('checkBalances', function () { + it('should succeed', async function () { + expect(await vault.checkBalances()).to.be.true + }) + }) + + describe('lock', function () { + let ERC20: Contract + const totalSupply = ethers.utils.parseEther('10') + beforeEach(async function () { + ERC20 = await deployContract('MockERC20', [owner.address, totalSupply]) + await ERC20.connect(owner).transfer(vault.address, ETHER) + }) + describe('with incorrect permission', function () { + it('should fail when wrong signer', async function () { + const permission = await signPermission( + 'Lock', + vault, + Wallet.createRandom(), + delegate.address, + ERC20.address, + ETHER, + ) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong function signature', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong delegate', async function () { + const permission = await signPermission('Lock', vault, owner, recipient.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong token', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, recipient.address, ETHER) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong amount', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong nonce', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER, 10) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + }) + describe('with correct permission', function () { + it('should succeed', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission) + }) + it('should create lock if new delegate-token pair', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission) + + expect(await vault.getLockSetCount()).to.be.eq(1) + const lockData = await vault.getLockAt(0) + expect(lockData.delegate).to.be.eq(delegate.address) + expect(lockData.token).to.be.eq(ERC20.address) + expect(lockData.balance).to.be.eq(ETHER) + expect(await vault.getBalanceDelegated(ERC20.address, delegate.address)).to.be.eq(ETHER) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(ETHER) + }) + it('should update lock if existing delegate-token pair', async function () { + const permission1 = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await vault.connect(delegate).lock(ERC20.address, ETHER.div(2), permission1) + + const permission2 = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await vault.connect(delegate).lock(ERC20.address, ETHER.div(2), permission2) + + expect(await vault.getLockSetCount()).to.be.eq(1) + const lockData = await vault.getLockAt(0) + expect(lockData.delegate).to.be.eq(delegate.address) + expect(lockData.token).to.be.eq(ERC20.address) + expect(lockData.balance).to.be.eq(ETHER) + expect(await vault.getBalanceDelegated(ERC20.address, delegate.address)).to.be.eq(ETHER) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(ETHER) + }) + it('should fail if insufficient vault balance on new lock', async function () { + const permission1 = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission1) + + const permission2 = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER.div(2), permission2)).to.be.revertedWith( + 'UniversalVault: insufficient balance', + ) + }) + it('should fail if insufficient vault balance on existing lock', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER.mul(2)) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER.mul(2), permission)).to.be.revertedWith( + 'UniversalVault: insufficient balance', + ) + }) + it('should bump nonce', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission) + + expect(await vault.getNonce()).to.be.eq(1) + }) + it('should emit event', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)) + .to.emit(vault, 'Locked') + .withArgs(delegate.address, ERC20.address, ETHER) + }) + }) + describe('when owner is ERC1271 compatible smart contract', function () { + let MockSmartWallet: Contract + beforeEach(async function () { + MockSmartWallet = await deployContract('MockSmartWallet', [owner.address]) + await factory.connect(owner).transferFrom(owner.address, MockSmartWallet.address, vault.address) + }) + describe('with valid wallet', function () { + it('should succeed', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission) + }) + it('should emit event', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)) + .to.emit(vault, 'Locked') + .withArgs(delegate.address, ERC20.address, ETHER) + }) + }) + describe('with invalid wallet', function () { + it('should fail', async function () { + const permission = await signPermission( + 'Lock', + vault, + Wallet.createRandom(), + delegate.address, + ERC20.address, + ETHER, + ) + await expect(vault.connect(delegate).lock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + }) + }) + }) + + describe('unlock', function () { + let ERC20: Contract + const totalSupply = ethers.utils.parseEther('10') + beforeEach(async function () { + ERC20 = await deployContract('MockERC20', [owner.address, totalSupply]) + await ERC20.connect(owner).transfer(vault.address, ETHER) + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission) + }) + describe('with incorrect permission', function () { + it('should fail when wrong signer', async function () { + const permission = await signPermission( + 'Unlock', + vault, + Wallet.createRandom(), + delegate.address, + ERC20.address, + ETHER, + ) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong function signature', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong delegate', async function () { + const permission = await signPermission('Unlock', vault, owner, recipient.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong token', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, recipient.address, ETHER) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong amount', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + it('should fail when wrong nonce', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER, 10) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + }) + describe('with correct permission', function () { + it('should succeed', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).unlock(ERC20.address, ETHER, permission) + }) + it('should fail if lock does not exist', async function () { + const permission = await signPermission('Unlock', vault, owner, recipient.address, ERC20.address, ETHER) + await expect(vault.connect(recipient).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'UniversalVault: missing lock', + ) + }) + it('should update lock balance if amount < balance', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await vault.connect(delegate).unlock(ERC20.address, ETHER.div(2), permission) + + expect(await vault.getLockSetCount()).to.be.eq(1) + const lockData = await vault.getLockAt(0) + expect(lockData.delegate).to.be.eq(delegate.address) + expect(lockData.token).to.be.eq(ERC20.address) + expect(lockData.balance).to.be.eq(ETHER.div(2)) + expect(await vault.getBalanceDelegated(ERC20.address, delegate.address)).to.be.eq(ETHER.div(2)) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(ETHER.div(2)) + }) + it('should delete lock if amount >= balance', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER.mul(2)) + await vault.connect(delegate).unlock(ERC20.address, ETHER.mul(2), permission) + + expect(await vault.getLockSetCount()).to.be.eq(0) + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + expect(await vault.getBalanceDelegated(ERC20.address, delegate.address)).to.be.eq(0) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(0) + }) + it('should bump nonce', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).unlock(ERC20.address, ETHER, permission) + + expect(await vault.getNonce()).to.be.eq(2) + }) + it('should emit event', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)) + .to.emit(vault, 'Unlocked') + .withArgs(delegate.address, ERC20.address, ETHER) + }) + }) + describe('when owner is ERC1271 compatible smart contract', function () { + let MockSmartWallet: Contract + beforeEach(async function () { + MockSmartWallet = await deployContract('MockSmartWallet', [owner.address]) + await factory.connect(owner).transferFrom(owner.address, MockSmartWallet.address, vault.address) + }) + describe('with valid wallet', function () { + it('should succeed', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).unlock(ERC20.address, ETHER, permission) + }) + it('should emit event', async function () { + const permission = await signPermission('Unlock', vault, owner, delegate.address, ERC20.address, ETHER) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)) + .to.emit(vault, 'Unlocked') + .withArgs(delegate.address, ERC20.address, ETHER) + }) + }) + describe('with invalid signature', function () { + it('should fail', async function () { + const permission = await signPermission( + 'Unlock', + vault, + Wallet.createRandom(), + delegate.address, + ERC20.address, + ETHER, + ) + await expect(vault.connect(delegate).unlock(ERC20.address, ETHER, permission)).to.be.revertedWith( + 'ERC1271: Invalid signature', + ) + }) + }) + }) + }) + + describe('rageQuit', function () { + let ERC20: Contract, MockDelegate: Contract + const totalSupply = ethers.utils.parseEther('10') + beforeEach(async function () { + ERC20 = await deployContract('MockERC20', [owner.address, totalSupply]) + await ERC20.connect(owner).transfer(vault.address, ETHER) + MockDelegate = await deployContract('MockDelegate') + const permission = await signPermission('Lock', vault, owner, MockDelegate.address, ERC20.address, ETHER) + await MockDelegate.lock(vault.address, ERC20.address, ETHER, permission) + }) + describe('as non-owner', function () { + beforeEach(async function () { + await MockDelegate.setDelegateType(DelegateType.Succeed) + }) + it('should fail', async function () { + await expect(vault.connect(recipient).rageQuit(delegate.address, ERC20.address)).to.be.revertedWith( + 'OwnableERC721: caller is not the owner', + ) + }) + }) + describe('with insufficient gas forwarded', function () { + let gasLimit: number + beforeEach(async function () { + gasLimit = (await vault.RAGEQUIT_GAS()).toNumber() + await MockDelegate.setDelegateType(DelegateType.Succeed) + }) + it('should fail', async function () { + await expect( + vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address, { gasLimit }), + ).to.be.revertedWith('UniversalVault: insufficient gas') + }) + }) + describe('delegate with success', function () { + beforeEach(async function () { + await MockDelegate.setDelegateType(DelegateType.Succeed) + }) + it('should succeed', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + }) + it('should fail when lock does not exist', async function () { + await expect(vault.connect(owner).rageQuit(recipient.address, ERC20.address)).to.be.revertedWith( + 'UniversalVault: missing lock', + ) + }) + it('should delete lock data', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + + expect(await vault.getLockSetCount()).to.be.eq(0) + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + expect(await vault.getBalanceDelegated(ERC20.address, MockDelegate.address)).to.be.eq(0) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(0) + }) + it('should emit event', async function () { + await expect(vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address)) + .to.emit(vault, 'RageQuit') + .withArgs(MockDelegate.address, ERC20.address, true, '') + }) + it('should return data', async function () { + const returnData = await vault.connect(owner).callStatic.rageQuit(MockDelegate.address, ERC20.address) + + expect(returnData.notified).to.be.true + expect(returnData.error).to.be.eq('') + }) + }) + describe('delegate with revert', function () { + beforeEach(async function () { + await MockDelegate.setDelegateType(DelegateType.Revert) + }) + it('should succeed', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + }) + it('should delete lock data', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + + expect(await vault.getLockSetCount()).to.be.eq(0) + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + expect(await vault.getBalanceDelegated(ERC20.address, MockDelegate.address)).to.be.eq(0) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(0) + }) + it('should emit event', async function () { + await expect(vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address)) + .to.emit(vault, 'RageQuit') + .withArgs(MockDelegate.address, ERC20.address, false, '') + }) + it('should return data', async function () { + const returnData = await vault.connect(owner).callStatic.rageQuit(MockDelegate.address, ERC20.address) + + expect(returnData.notified).to.be.false + expect(returnData.error).to.be.eq('') + }) + }) + describe('delegate with revert message', function () { + beforeEach(async function () { + await MockDelegate.setDelegateType(DelegateType.RevertWithMessage) + }) + it('should succeed', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + }) + it('should delete lock data', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + + expect(await vault.getLockSetCount()).to.be.eq(0) + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + expect(await vault.getBalanceDelegated(ERC20.address, MockDelegate.address)).to.be.eq(0) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(0) + }) + it('should emit event', async function () { + await expect(vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address)) + .to.emit(vault, 'RageQuit') + .withArgs(MockDelegate.address, ERC20.address, false, 'MockDelegate: revert with message') + }) + it('should return data', async function () { + const returnData = await vault.connect(owner).callStatic.rageQuit(MockDelegate.address, ERC20.address) + + expect(returnData.notified).to.be.false + expect(returnData.error).to.be.eq('MockDelegate: revert with message') + }) + }) + describe('delegate with out of gas error', function () { + beforeEach(async function () { + await MockDelegate.setDelegateType(DelegateType.OOG) + }) + it('should succeed', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + }) + it('should delete lock data', async function () { + await vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address) + + expect(await vault.getLockSetCount()).to.be.eq(0) + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + expect(await vault.getBalanceDelegated(ERC20.address, MockDelegate.address)).to.be.eq(0) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(0) + }) + it('should emit event', async function () { + await expect(vault.connect(owner).rageQuit(MockDelegate.address, ERC20.address)) + .to.emit(vault, 'RageQuit') + .withArgs(MockDelegate.address, ERC20.address, false, '') + }) + it('should return data', async function () { + const returnData = await vault.connect(owner).callStatic.rageQuit(MockDelegate.address, ERC20.address) + + expect(returnData.notified).to.be.false + expect(returnData.error).to.be.eq('') + }) + }) + describe('delegate is EOA', function () { + beforeEach(async function () { + await MockDelegate.unlock( + vault.address, + ERC20.address, + ETHER, + await signPermission('Unlock', vault, owner, MockDelegate.address, ERC20.address, ETHER), + ) + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER) + await vault.connect(delegate).lock(ERC20.address, ETHER, permission) + }) + it('should succeed', async function () { + await vault.connect(owner).rageQuit(delegate.address, ERC20.address) + }) + it('should delete lock data', async function () { + await vault.connect(owner).rageQuit(delegate.address, ERC20.address) + + expect(await vault.getLockSetCount()).to.be.eq(0) + await expect(vault.getLockAt(0)).to.be.revertedWith('EnumerableSet: index out of bounds') + expect(await vault.getBalanceDelegated(ERC20.address, delegate.address)).to.be.eq(0) + expect(await vault.getBalanceLocked(ERC20.address)).to.be.eq(0) + }) + it('should emit event', async function () { + await expect(vault.connect(owner).rageQuit(delegate.address, ERC20.address)) + .to.emit(vault, 'RageQuit') + .withArgs(delegate.address, ERC20.address, false, '') + }) + it('should return data', async function () { + const returnData = await vault.connect(owner).callStatic.rageQuit(delegate.address, ERC20.address) + + expect(returnData.notified).to.be.false + expect(returnData.error).to.be.eq('') + }) + }) + }) + describe('ERC20', function () { + let ERC20: Contract + const totalSupply = ethers.utils.parseEther('10') + beforeEach(async function () { + ERC20 = await deployContract('MockERC20', [owner.address, totalSupply]) + await ERC20.connect(owner).transfer(vault.address, ETHER) + }) + describe('ERC20:transfer', function () { + it('should succeed', async function () { + await vault.connect(owner).transferERC20(ERC20.address, recipient.address, ETHER) + }) + it('should transfer tokens', async function () { + await vault.connect(owner).transferERC20(ERC20.address, recipient.address, ETHER) + + expect(await ERC20.balanceOf(recipient.address)).to.be.eq(ETHER) + }) + it('should fail if insufficient unlocked balance', async function () { + const permission = await signPermission('Lock', vault, owner, delegate.address, ERC20.address, ETHER.div(2)) + await vault.connect(delegate).lock(ERC20.address, ETHER.div(2), permission) + + await expect(vault.connect(owner).transferERC20(ERC20.address, recipient.address, ETHER)).to.be.revertedWith( + 'UniversalVault: insufficient balance', + ) + }) + }) + }) + describe('ETH', function () { + describe('ETH:receive', function () { + it('should succeed', async function () { + await owner.sendTransaction({ + to: vault.address, + value: ETHER, + }) + }) + it('should receive correct amount', async function () { + await owner.sendTransaction({ + to: vault.address, + value: ETHER, + }) + expect(await ethers.provider.getBalance(vault.address)).to.eq(ETHER) + }) + }) + describe('ETH:send', function () { + it('should succeed', async function () { + await vault.connect(owner).transferETH(recipient.address, ETHER, { value: ETHER }) + }) + it('should send correct amount', async function () { + await vault.connect(owner).transferETH(recipient.address, ETHER, { value: ETHER }) + expect(await ethers.provider.getBalance(vault.address)).to.eq(0) + }) + it('should fail if insufficient amount', async function () { + await expect(vault.connect(owner).transferETH(recipient.address, ETHER)).to.be.revertedWith('le') + }) + }) + }) +}) diff --git a/test/VaultFactory.ts b/test/VaultFactory.ts new file mode 100644 index 0000000..7cd1863 --- /dev/null +++ b/test/VaultFactory.ts @@ -0,0 +1,43 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { ethers } from 'hardhat' +import { create2Instance, createInstance, deployContract } from './utils' + +describe('VaultFactory', function () { + let accounts: SignerWithAddress[] + let factory: Contract, template: Contract + + beforeEach(async function () { + // prepare signers + accounts = await ethers.getSigners() + // deploy template + template = await deployContract('UniversalVault') + // deploy factory + factory = await deployContract('VaultFactory', [template.address]) + }) + + describe('getTemplate', function () { + it('should succeed', async function () { + expect(await factory.getTemplate()).to.be.eq(template.address) + }) + }) + describe('create', function () { + it('should succeed', async function () { + await createInstance('UniversalVault', factory, accounts[0]) + }) + it('should successfully call owner', async function () { + const vault = await createInstance('UniversalVault', factory, accounts[0]) + expect(await vault.owner()).to.eq(accounts[0].address) + }) + }) + describe('create2', function () { + it('should succeed', async function () { + await create2Instance('UniversalVault', factory, accounts[0], ethers.utils.randomBytes(32)) + }) + it('should successfully call owner', async function () { + const vault = await create2Instance('UniversalVault', factory, accounts[0], ethers.utils.randomBytes(32)) + expect(await vault.owner()).to.eq(accounts[0].address) + }) + }) +}) diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..32c05f9 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,133 @@ +import { TypedDataField } from '@ethersproject/abstract-signer' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { BigNumberish, BytesLike, Contract, Signer, Wallet } from 'ethers' +import { ethers, network, upgrades } from 'hardhat' + +export async function getTimestamp() { + return (await ethers.provider.getBlock('latest')).timestamp +} + +export async function increaseTime(seconds: number) { + const time = await getTimestamp() + // instead of using evm_increaseTime, we can pass in the timestamp + // the next block should setup as the mining time + const expectedEndTime = time + seconds - 1 + await network.provider.request({ + method: 'evm_mine', + params: [expectedEndTime], + }) + if (expectedEndTime !== (await getTimestamp())) { + throw new Error('evm_mine failed') + } +} + +// Perc has to be a whole number +export async function invokeRebase(ampl: Contract, perc: number, orchestrator: Signer) { + const PERC_DECIMALS = 2 + const s = await ampl.totalSupply.call() + const ordinate = 10 ** PERC_DECIMALS + const p_ = ethers.BigNumber.from(perc * ordinate).div(100) + const s_ = s.mul(p_).div(ordinate) + await ampl.connect(orchestrator).rebase(1, s_) +} + +export async function deployContract(name: string, args: Array = []) { + const factory = await ethers.getContractFactory(name) + const contract = await factory.deploy(...args) + return contract.deployed() +} + +export async function deployAmpl(admin: SignerWithAddress) { + const factory = await ethers.getContractFactory('MockAmpl') + const ampl = await upgrades.deployProxy(factory, [admin.address], { + initializer: 'initialize(address)', + }) + await ampl.connect(admin).setMonetaryPolicy(admin.address) + const amplInitialSupply = await ampl.balanceOf(admin.address) + return { ampl, amplInitialSupply } +} + +export async function deployGeyser(args: Array) { + const factory = await ethers.getContractFactory('Geyser') + return upgrades.deployProxy(factory, args, { + unsafeAllowCustomTypes: true, + }) +} + +export async function createInstance(instanceName: string, factory: Contract, signer: Signer, args: string = '0x') { + // get contract class + const instance = await ethers.getContractAt( + instanceName, + await factory.connect(signer).callStatic['create(bytes)'](args), + ) + // deploy vault + await factory.connect(signer)['create(bytes)'](args) + // return contract class + return instance +} + +export async function create2Instance( + instanceName: string, + factory: Contract, + signer: Signer, + salt: BytesLike, + args: string = '0x', +) { + // get contract class + const instance = await ethers.getContractAt( + instanceName, + await factory.connect(signer).callStatic['create2(bytes,bytes32)'](args, salt), + ) + // deploy vault + await factory.connect(signer)['create2(bytes,bytes32)'](args, salt) + // return contract class + return instance +} + +export const signPermission = async ( + method: string, + vault: Contract, + owner: Wallet, + delegateAddress: string, + tokenAddress: string, + amount: BigNumberish, + vaultNonce?: BigNumberish, + chainId?: BigNumberish, +) => { + // get nonce + vaultNonce = vaultNonce || (await vault.getNonce()) + // get chainId + chainId = chainId || (await vault.provider.getNetwork()).chainId + // craft permission + const domain = { + name: 'UniversalVault', + version: '1.0.0', + chainId, + verifyingContract: vault.address, + } + const types = {} as Record + types[method] = [ + { name: 'delegate', type: 'address' }, + { name: 'token', type: 'address' }, + { name: 'amount', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + ] + const value = { + delegate: delegateAddress, + token: tokenAddress, + amount: amount, + nonce: vaultNonce, + } + // sign permission + const signedPermission = await owner._signTypedData(domain, types, value) + // return + return signedPermission +} + +export const transferNFT = async (nft: Contract, signer: Signer, owner: string, recipient: string, tokenId: string) => { + return nft.connect(signer)['safeTransferFrom(address,address,uint256)'](owner, recipient, tokenId) +} + +export const ERC1271_VALID_SIG = '0x1626ba7e' +export const ERC1271_INVALID_SIG = '0xffffffff' +export const ETHER = ethers.utils.parseEther('1') From 38b957a78929df595f454b4bceb953a777980d11 Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 00:48:42 +0100 Subject: [PATCH 02/10] deploy mist and aludel instead of ampl and geyser --- test/utils.ts | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/test/utils.ts b/test/utils.ts index 32c05f9..002b3a2 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,7 +1,11 @@ import { TypedDataField } from '@ethersproject/abstract-signer' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' -import { BigNumberish, BytesLike, Contract, Signer, Wallet } from 'ethers' +import { BigNumber, BigNumberish, BytesLike, Contract, Signer, Wallet } from 'ethers' import { ethers, network, upgrades } from 'hardhat' +import '@openzeppelin/hardhat-upgrades' +import { parseEther } from 'ethers/lib/utils' + +const DAY = 60 * 60 * 24 export async function getTimestamp() { return (await ethers.provider.getBlock('latest')).timestamp @@ -47,13 +51,37 @@ export async function deployAmpl(admin: SignerWithAddress) { return { ampl, amplInitialSupply } } -export async function deployGeyser(args: Array) { - const factory = await ethers.getContractFactory('Geyser') - return upgrades.deployProxy(factory, args, { - unsafeAllowCustomTypes: true, - }) +export async function deployMist(admin: SignerWithAddress) { + const factory = await ethers.getContractFactory('Alchemist') + + // todo : use the token manager + const tokenManager = await (await ethers.getContractFactory('TokenManager', admin)).deploy() + + const now = new Date() + const mist = await factory.deploy( + admin.address, + admin.address, + 1000, + BigNumber.from(14).mul(DAY), + BigNumber.from(60).mul(DAY), + parseEther("1000"), //parseEther(1000000*10**18), + Math.round(now.getTime() / 1000) + ) + + await mist.connect(admin) + const initialSupply = await mist.balanceOf(admin.address) + + return { + mist, + initialSupply + } } + export async function deployAludel(args: Array) { + const factory = await ethers.getContractFactory('Aludel') + return factory.deploy(...args) + } + export async function createInstance(instanceName: string, factory: Contract, signer: Signer, args: string = '0x') { // get contract class const instance = await ethers.getContractAt( From f9ff5fb6b4c3c955c1612e1073ec94e6e3521c47 Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 01:02:57 +0100 Subject: [PATCH 03/10] update references from geyser to aludel and vault to crucible. --- test/Geyser.ts | 276 ++++++++++++++++++++--------------------- test/UniversalVault.ts | 8 +- test/VaultFactory.ts | 12 +- 3 files changed, 148 insertions(+), 148 deletions(-) diff --git a/test/Geyser.ts b/test/Geyser.ts index 1415ee4..40e40f8 100644 --- a/test/Geyser.ts +++ b/test/Geyser.ts @@ -130,8 +130,8 @@ describe('Aludel', function () { // deploy dependencies powerSwitchFactory = await deployContract('PowerSwitchFactory') rewardPoolFactory = await deployContract('RewardPoolFactory') - vaultTemplate = await deployContract('UniversalVault') - vaultFactory = await deployContract('VaultFactory', [vaultTemplate.address]) + vaultTemplate = await deployContract('Crucible') + vaultFactory = await deployContract('CrucibleFactory', [vaultTemplate.address]) // deploy mock tokens stakingToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) @@ -178,7 +178,7 @@ describe('Aludel', function () { ] const geyser = await deployAludel(args) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.stakingToken).to.eq(stakingToken.address) expect(data.rewardToken).to.eq(rewardToken.address) @@ -214,19 +214,19 @@ describe('Aludel', function () { ] geyser = await deployAludel(args) powerSwitch = await ethers.getContractAt('PowerSwitch', await geyser.getPowerSwitch()) - rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getGeyserData()).rewardPool) + rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getAludelData()).rewardPool) }) - describe('fundGeyser', function () { + describe('fundAludel', function () { describe('with insufficient approval', function () { it('should fail', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)).to.be.reverted + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)).to.be.reverted }) }) describe('with duration of zero', function () { it('should fail', async function () { await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, 0)).to.be.revertedWith( - 'Geyser: invalid duration', + await expect(geyser.connect(admin).fund(amplInitialSupply, 0)).to.be.revertedWith( + 'Aludel: invalid duration', ) }) }) @@ -234,7 +234,7 @@ describe('Aludel', function () { it('should fail', async function () { await rewardToken.connect(admin).transfer(user.address, amplInitialSupply) await rewardToken.connect(user).approve(geyser.address, amplInitialSupply) - await expect(geyser.connect(user).fundGeyser(amplInitialSupply, YEAR)).to.be.revertedWith( + await expect(geyser.connect(user).fund(amplInitialSupply, YEAR)).to.be.revertedWith( 'Ownable: caller is not the owner', ) }) @@ -243,7 +243,7 @@ describe('Aludel', function () { it('should fail', async function () { await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) await powerSwitch.connect(admin).powerOff() - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)).to.be.revertedWith( + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)).to.be.revertedWith( 'Powered: is not online', ) }) @@ -252,7 +252,7 @@ describe('Aludel', function () { it('should fail', async function () { await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) await powerSwitch.connect(admin).emergencyShutdown() - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)).to.be.revertedWith( + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)).to.be.revertedWith( 'Powered: is not online', ) }) @@ -263,12 +263,12 @@ describe('Aludel', function () { }) describe('at first funding', function () { it('should succeed', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + await geyser.connect(admin).fund(amplInitialSupply, YEAR) }) it('should update state correctly', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + await geyser.connect(admin).fund(amplInitialSupply, YEAR) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) expect(data.rewardSchedules.length).to.eq(1) @@ -277,28 +277,28 @@ describe('Aludel', function () { expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) }) it('should emit event', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) - .to.emit(geyser, 'GeyserFunded') + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)) + .to.emit(geyser, 'AludelFunded') .withArgs(amplInitialSupply, YEAR) }) it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)) .to.emit(rewardToken, 'Transfer') .withArgs(admin.address, rewardPool.address, amplInitialSupply) }) }) describe('at second funding', function () { beforeEach(async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR) + await geyser.connect(admin).fund(amplInitialSupply.div(2), YEAR) }) describe('with no rebase', function () { it('should succeed', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR) + await geyser.connect(admin).fund(amplInitialSupply.div(2), YEAR) }) it('should update state correctly', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR) + await geyser.connect(admin).fund(amplInitialSupply.div(2), YEAR) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) expect(data.rewardSchedules.length).to.eq(2) @@ -310,12 +310,12 @@ describe('Aludel', function () { expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) }) it('should emit event', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR)) - .to.emit(geyser, 'GeyserFunded') + await expect(geyser.connect(admin).fund(amplInitialSupply.div(2), YEAR)) + .to.emit(geyser, 'AludelFunded') .withArgs(amplInitialSupply.div(2), YEAR) }) it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), YEAR)) + await expect(geyser.connect(admin).fund(amplInitialSupply.div(2), YEAR)) .to.emit(rewardToken, 'Transfer') .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) }) @@ -327,12 +327,12 @@ describe('Aludel', function () { await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) }) it('should succeed', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + await geyser.connect(admin).fund(amplInitialSupply, YEAR) }) it('should update state correctly', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + await geyser.connect(admin).fund(amplInitialSupply, YEAR) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) expect(data.rewardSchedules.length).to.eq(2) @@ -344,12 +344,12 @@ describe('Aludel', function () { expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) }) it('should emit event', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) - .to.emit(geyser, 'GeyserFunded') + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)) + .to.emit(geyser, 'AludelFunded') .withArgs(amplInitialSupply, YEAR) }) it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR)) + await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)) .to.emit(rewardToken, 'Transfer') .withArgs(admin.address, rewardPool.address, amplInitialSupply) }) @@ -360,12 +360,12 @@ describe('Aludel', function () { await invokeRebase(rewardToken, -50, admin) }) it('should succeed', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR) + await geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR) }) it('should update state correctly', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR) + await geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) expect(data.rewardSchedules.length).to.eq(2) @@ -377,12 +377,12 @@ describe('Aludel', function () { expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) }) it('should emit event', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR)) - .to.emit(geyser, 'GeyserFunded') + await expect(geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR)) + .to.emit(geyser, 'AludelFunded') .withArgs(amplInitialSupply.div(4), YEAR) }) it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(4), YEAR)) + await expect(geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR)) .to.emit(rewardToken, 'Transfer') .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(4)) }) @@ -394,7 +394,7 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await geyser.connect(admin).registerVaultFactory(vaultFactory.address) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -403,7 +403,7 @@ describe('Aludel', function () { await increaseTime(rewardScaling.time) await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + await geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time) }) describe('with partial rewards exausted', function () { beforeEach(async function () { @@ -411,12 +411,12 @@ describe('Aludel', function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) }) it('should succeed', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + await geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time) }) it('should update state correctly', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + await geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).mul(3).div(4)) expect(data.rewardSchedules.length).to.eq(2) @@ -427,12 +427,12 @@ describe('Aludel', function () { expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) }) it('should emit event', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) - .to.emit(geyser, 'GeyserFunded') + await expect(geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time)) + .to.emit(geyser, 'AludelFunded') .withArgs(amplInitialSupply.div(2), rewardScaling.time) }) it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) + await expect(geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time)) .to.emit(rewardToken, 'Transfer') .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) }) @@ -443,12 +443,12 @@ describe('Aludel', function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) }) it('should succeed', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + await geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time) }) it('should update state correctly', async function () { - await geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time) + await geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time) - const data = await geyser.getGeyserData() + const data = await geyser.getAludelData() expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) expect(data.rewardSchedules.length).to.eq(2) @@ -459,12 +459,12 @@ describe('Aludel', function () { expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) }) it('should emit event', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) - .to.emit(geyser, 'GeyserFunded') + await expect(geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time)) + .to.emit(geyser, 'AludelFunded') .withArgs(amplInitialSupply.div(2), rewardScaling.time) }) it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fundGeyser(amplInitialSupply.div(2), rewardScaling.time)) + await expect(geyser.connect(admin).fund(amplInitialSupply.div(2), rewardScaling.time)) .to.emit(rewardToken, 'Transfer') .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) }) @@ -476,7 +476,7 @@ describe('Aludel', function () { describe('isValidVault', function () { let vault: Contract beforeEach(async function () { - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) }) describe('when no factory registered', function () { it('should be false', async function () { @@ -505,8 +505,8 @@ describe('Aludel', function () { let secondVault: Contract beforeEach(async function () { await geyser.connect(admin).registerVaultFactory(vaultFactory.address) - secondFactory = await deployContract('VaultFactory', [vaultTemplate.address]) - secondVault = await createInstance('UniversalVault', secondFactory, user) + secondFactory = await deployContract('CrucibleFactory', [vaultTemplate.address]) + secondVault = await createInstance('Crucible', secondFactory, user) }) it('should be false', async function () { expect(await geyser.isValidVault(secondVault.address)).to.be.false @@ -516,8 +516,8 @@ describe('Aludel', function () { let secondFactory: Contract let secondVault: Contract beforeEach(async function () { - secondFactory = await deployContract('VaultFactory', [vaultTemplate.address]) - secondVault = await createInstance('UniversalVault', secondFactory, user) + secondFactory = await deployContract('CrucibleFactory', [vaultTemplate.address]) + secondVault = await createInstance('Crucible', secondFactory, user) await geyser.connect(admin).registerVaultFactory(vaultFactory.address) await geyser.connect(admin).registerVaultFactory(secondFactory.address) }) @@ -587,7 +587,7 @@ describe('Aludel', function () { }) it('should fail', async function () { await expect(geyser.connect(admin).registerVaultFactory(vaultFactory.address)).to.be.revertedWith( - 'Geyser: vault factory already registered', + 'Aludel: vault factory already registered', ) }) }) @@ -695,7 +695,7 @@ describe('Aludel', function () { describe('when never added', function () { it('should fail', async function () { await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)).to.be.revertedWith( - 'Geyser: vault factory not registered', + 'Aludel: vault factory not registered', ) }) }) @@ -706,7 +706,7 @@ describe('Aludel', function () { }) it('should fail', async function () { await expect(geyser.connect(admin).removeVaultFactory(vaultFactory.address)).to.be.revertedWith( - 'Geyser: vault factory not registered', + 'Aludel: vault factory not registered', ) }) }) @@ -725,35 +725,35 @@ describe('Aludel', function () { describe('with address zero', function () { it('should fail', async function () { await expect(geyser.connect(admin).registerBonusToken(ethers.constants.AddressZero)).to.be.revertedWith( - 'Geyser: invalid address', + 'Aludel: invalid address', ) }) }) describe('with geyser address', function () { it('should fail', async function () { await expect(geyser.connect(admin).registerBonusToken(geyser.address)).to.be.revertedWith( - 'Geyser: invalid address', + 'Aludel: invalid address', ) }) }) describe('with staking token', function () { it('should fail', async function () { await expect(geyser.connect(admin).registerBonusToken(stakingToken.address)).to.be.revertedWith( - 'Geyser: invalid address', + 'Aludel: invalid address', ) }) }) describe('with reward token', function () { it('should fail', async function () { await expect(geyser.connect(admin).registerBonusToken(rewardToken.address)).to.be.revertedWith( - 'Geyser: invalid address', + 'Aludel: invalid address', ) }) }) describe('with rewardPool address', function () { it('should fail', async function () { await expect(geyser.connect(admin).registerBonusToken(rewardPool.address)).to.be.revertedWith( - 'Geyser: invalid address', + 'Aludel: invalid address', ) }) }) @@ -780,7 +780,7 @@ describe('Aludel', function () { describe('with same token', function () { it('should fail', async function () { await expect(geyser.connect(admin).registerBonusToken(bonusToken.address)).to.be.revertedWith( - 'Geyser: invalid address', + 'Aludel: invalid address', ) }) }) @@ -842,14 +842,14 @@ describe('Aludel', function () { it('should fail', async function () { await expect( geyser.connect(admin).rescueTokensFromRewardPool(rewardToken.address, admin.address, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with bonus token', function () { it('should fail', async function () { await expect( geyser.connect(admin).rescueTokensFromRewardPool(bonusToken.address, admin.address, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with staking token', function () { @@ -871,28 +871,28 @@ describe('Aludel', function () { it('should fail', async function () { await expect( geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, geyser.address, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with staking token as recipient', function () { it('should fail', async function () { await expect( geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, stakingToken.address, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with reward token as recipient', function () { it('should fail', async function () { await expect( geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, rewardToken.address, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with rewardPool as recipient', function () { it('should fail', async function () { await expect( geyser.connect(admin).rescueTokensFromRewardPool(otherToken.address, rewardPool.address, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with address 0 as recipient', function () { @@ -901,7 +901,7 @@ describe('Aludel', function () { geyser .connect(admin) .rescueTokensFromRewardPool(otherToken.address, ethers.constants.AddressZero, mockTokenSupply), - ).to.be.revertedWith('Geyser: invalid address') + ).to.be.revertedWith('Aludel: invalid address') }) }) describe('with other address as recipient', function () { @@ -1008,7 +1008,7 @@ describe('Aludel', function () { geyser = await deployAludel(args) await geyser.connect(admin).registerVaultFactory(vaultFactory.address) powerSwitch = await ethers.getContractAt('PowerSwitch', await geyser.getPowerSwitch()) - rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getGeyserData()).rewardPool) + rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getAludelData()).rewardPool) }) describe('stake', function () { @@ -1016,7 +1016,7 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) }) describe('when offline', function () { @@ -1039,19 +1039,19 @@ describe('Aludel', function () { it('should fail', async function () { await geyser.connect(admin).removeVaultFactory(vaultFactory.address) await expect(stake(user, geyser, vault, stakingToken, stakeAmount)).to.be.revertedWith( - 'Geyser: vault is not registered', + 'Aludel: vault is not registered', ) }) }) describe('with amount of zero', function () { it('should fail', async function () { - await expect(stake(user, geyser, vault, stakingToken, '0')).to.be.revertedWith('Geyser: no amount staked') + await expect(stake(user, geyser, vault, stakingToken, '0')).to.be.revertedWith('Aludel: no amount staked') }) }) describe('with insufficient balance', function () { it('should fail', async function () { await expect(stake(user, geyser, vault, stakingToken, stakeAmount.mul(2))).to.be.revertedWith( - 'UniversalVault: insufficient balance', + 'Crucible: insufficient balance', ) }) }) @@ -1063,7 +1063,7 @@ describe('Aludel', function () { describe('when funded', function () { beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) - await geyser.connect(admin).fundGeyser(amplInitialSupply, YEAR) + await geyser.connect(admin).fund(amplInitialSupply, YEAR) }) describe('on first stake', function () { describe('as vault owner', function () { @@ -1073,7 +1073,7 @@ describe('Aludel', function () { it('should update state', async function () { await stake(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.totalStake).to.eq(stakeAmount) @@ -1107,7 +1107,7 @@ describe('Aludel', function () { it('should update state', async function () { await stake(user, geyser, vault, stakingToken, stakeAmount.div(2)) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.totalStake).to.eq(stakeAmount) @@ -1142,7 +1142,7 @@ describe('Aludel', function () { }) it('should fail', async function () { await expect(stake(user, geyser, vault, stakingToken, stakeAmount.div(quantity))).to.be.revertedWith( - 'Geyser: MAX_STAKES_PER_VAULT reached', + 'Aludel: MAX_STAKES_PER_VAULT reached', ) }) }) @@ -1158,7 +1158,7 @@ describe('Aludel', function () { it('should update state', async function () { await stake(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.totalStake).to.eq(stakeAmount) @@ -1191,11 +1191,11 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1234,14 +1234,14 @@ describe('Aludel', function () { describe('with amount of zero', function () { it('should fail', async function () { await expect(unstakeAndClaim(user, geyser, vault, stakingToken, 0)).to.be.revertedWith( - 'Geyser: no amount unstaked', + 'Aludel: no amount unstaked', ) }) }) describe('with amount greater than stakes', function () { it('should fail', async function () { await expect(unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.add(1))).to.be.revertedWith( - 'Geyser: insufficient vault stake', + 'Aludel: insufficient vault stake', ) }) }) @@ -1250,11 +1250,11 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1268,7 +1268,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(0) @@ -1301,11 +1301,11 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1319,7 +1319,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1363,14 +1363,14 @@ describe('Aludel', function () { geyser = await deployAludel(args) await geyser.connect(admin).registerVaultFactory(vaultFactory.address) powerSwitch = await ethers.getContractAt('PowerSwitch', await geyser.getPowerSwitch()) - rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getGeyserData()).rewardPool) + rewardPool = await ethers.getContractAt('RewardPool', (await geyser.getAludelData()).rewardPool) await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1384,7 +1384,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1413,7 +1413,7 @@ describe('Aludel', function () { describe('with no reward', function () { let vault: Contract beforeEach(async function () { - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1427,7 +1427,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(0) @@ -1452,7 +1452,7 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1461,7 +1461,7 @@ describe('Aludel', function () { await increaseTime(rewardScaling.time) await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time / 2) }) @@ -1471,7 +1471,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1502,11 +1502,11 @@ describe('Aludel', function () { beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1546,7 +1546,7 @@ describe('Aludel', function () { ), ) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) @@ -1624,11 +1624,11 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1645,7 +1645,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1682,11 +1682,11 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -1700,7 +1700,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount.div(2)) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1742,12 +1742,12 @@ describe('Aludel', function () { beforeEach(async function () { // fund geyser await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) // deploy vault and transfer stake - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, currentStake) // perform multiple stakes in same block @@ -1782,7 +1782,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1826,12 +1826,12 @@ describe('Aludel', function () { beforeEach(async function () { // fund geyser await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) // deploy vault and transfer stake - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, currentStake) // perform multiple stakes in same block @@ -1866,7 +1866,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -1904,12 +1904,12 @@ describe('Aludel', function () { beforeEach(async function () { // fund geyser await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) // deploy vault and transfer stake - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, currentStake) // perform multiple stakes in same block @@ -1944,7 +1944,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, unstakedAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(0) @@ -1974,13 +1974,13 @@ describe('Aludel', function () { let vault: Contract beforeEach(async function () { await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) await geyser.connect(admin).registerBonusToken(bonusToken.address) - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) }) @@ -1995,7 +1995,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(0) @@ -2036,7 +2036,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(0) @@ -2100,7 +2100,7 @@ describe('Aludel', function () { it('should update state', async function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.sub(expectedReward).mul(BASE_SHARES_PER_WEI)) @@ -2150,7 +2150,7 @@ describe('Aludel', function () { beforeEach(async function () { // fund geyser await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) await increaseTime(rewardScaling.time) @@ -2158,7 +2158,7 @@ describe('Aludel', function () { vaults = [] const permissions = [] for (let index = 0; index < quantity; index++) { - const vault = await createInstance('UniversalVault', vaultFactory, user) + const vault = await createInstance('Crucible', vaultFactory, user) await stakingToken.connect(admin).transfer(vault.address, stakeAmount) vaults.push(vault) @@ -2190,7 +2190,7 @@ describe('Aludel', function () { await unstakeAndClaim(user, geyser, vault, stakingToken, stakeAmount) } - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() expect(geyserData.rewardSharesOutstanding).to.eq(0) expect(geyserData.totalStake).to.eq(0) @@ -2232,10 +2232,10 @@ describe('Aludel', function () { beforeEach(async function () { // fund geyser await rewardToken.connect(admin).approve(geyser.address, rewardAmount) - await geyser.connect(admin).fundGeyser(rewardAmount, rewardScaling.time) + await geyser.connect(admin).fund(rewardAmount, rewardScaling.time) // create vault - vault = await createInstance('UniversalVault', vaultFactory, user) + vault = await createInstance('Crucible', vaultFactory, user) // stake await stakingToken.connect(admin).transfer(vault.address, stakeAmount) @@ -2252,7 +2252,7 @@ describe('Aludel', function () { gasLimit, }) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) @@ -2277,7 +2277,7 @@ describe('Aludel', function () { gasLimit, }) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) @@ -2302,7 +2302,7 @@ describe('Aludel', function () { gasLimit, }) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) @@ -2319,17 +2319,17 @@ describe('Aludel', function () { geyser.connect(user).rageQuit({ gasLimit, }), - ).to.be.revertedWith('Geyser: no stake') + ).to.be.revertedWith('Aludel: no stake') }) }) describe('when no stake', function () { it('should fail', async function () { - const secondVault = await createInstance('UniversalVault', vaultFactory, user) + const secondVault = await createInstance('Crucible', vaultFactory, user) await expect( secondVault.connect(user).rageQuit(geyser.address, stakingToken.address, { gasLimit, }), - ).to.be.revertedWith('UniversalVault: missing lock') + ).to.be.revertedWith('Crucible: missing lock') }) }) describe('when insufficient gas', function () { @@ -2338,7 +2338,7 @@ describe('Aludel', function () { vault.connect(user).rageQuit(geyser.address, stakingToken.address, { gasLimit: await vault.RAGEQUIT_GAS(), }), - ).to.be.revertedWith('UniversalVault: insufficient gas') + ).to.be.revertedWith('Crucible: insufficient gas') }) }) describe('when insufficient gas with multiple stakes', function () { @@ -2355,7 +2355,7 @@ describe('Aludel', function () { vault.connect(user).rageQuit(geyser.address, stakingToken.address, { gasLimit: await vault.RAGEQUIT_GAS(), }), - ).to.be.revertedWith('UniversalVault: insufficient gas') + ).to.be.revertedWith('Crucible: insufficient gas') }) }) describe('when single stake', function () { @@ -2369,7 +2369,7 @@ describe('Aludel', function () { gasLimit, }) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) @@ -2400,7 +2400,7 @@ describe('Aludel', function () { gasLimit, }) - const geyserData = await geyser.getGeyserData() + const geyserData = await geyser.getAludelData() const vaultData = await geyser.getVaultData(vault.address) expect(geyserData.rewardSharesOutstanding).to.eq(rewardAmount.mul(BASE_SHARES_PER_WEI)) diff --git a/test/UniversalVault.ts b/test/UniversalVault.ts index ddf8c96..3b83b6c 100644 --- a/test/UniversalVault.ts +++ b/test/UniversalVault.ts @@ -11,7 +11,7 @@ enum DelegateType { OOG, } -describe('UniversalVault', function () { +describe('Crucible', function () { let accounts: SignerWithAddress[], admin: SignerWithAddress, recipient: SignerWithAddress, delegate: SignerWithAddress let owner: Wallet let factory: Contract, vault: Contract @@ -31,13 +31,13 @@ describe('UniversalVault', function () { beforeEach(async function () { // deploy template - const template = await deployContract('UniversalVault') + const template = await deployContract('Crucible') // deploy factory - factory = await deployContract('VaultFactory', [template.address]) + factory = await deployContract('CrucibleFactory', [template.address]) // deploy instance - vault = await createInstance('UniversalVault', factory, owner) + vault = await createInstance('Crucible', factory, owner) }) describe('nft', function () { diff --git a/test/VaultFactory.ts b/test/VaultFactory.ts index 7cd1863..25c92f0 100644 --- a/test/VaultFactory.ts +++ b/test/VaultFactory.ts @@ -12,9 +12,9 @@ describe('VaultFactory', function () { // prepare signers accounts = await ethers.getSigners() // deploy template - template = await deployContract('UniversalVault') + template = await deployContract('Crucible') // deploy factory - factory = await deployContract('VaultFactory', [template.address]) + factory = await deployContract('CrucibleFactory', [template.address]) }) describe('getTemplate', function () { @@ -24,19 +24,19 @@ describe('VaultFactory', function () { }) describe('create', function () { it('should succeed', async function () { - await createInstance('UniversalVault', factory, accounts[0]) + await createInstance('Crucible', factory, accounts[0]) }) it('should successfully call owner', async function () { - const vault = await createInstance('UniversalVault', factory, accounts[0]) + const vault = await createInstance('Crucible', factory, accounts[0]) expect(await vault.owner()).to.eq(accounts[0].address) }) }) describe('create2', function () { it('should succeed', async function () { - await create2Instance('UniversalVault', factory, accounts[0], ethers.utils.randomBytes(32)) + await create2Instance('Crucible', factory, accounts[0], ethers.utils.randomBytes(32)) }) it('should successfully call owner', async function () { - const vault = await create2Instance('UniversalVault', factory, accounts[0], ethers.utils.randomBytes(32)) + const vault = await create2Instance('Crucible', factory, accounts[0], ethers.utils.randomBytes(32)) expect(await vault.owner()).to.eq(accounts[0].address) }) }) From 1c382c9b64913235844f22e3e482e3b73c0b51f5 Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 01:04:30 +0100 Subject: [PATCH 04/10] remove deployAmpl function --- test/utils.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/utils.ts b/test/utils.ts index 002b3a2..9f20f98 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -41,16 +41,6 @@ export async function deployContract(name: string, args: Array = []) { return contract.deployed() } -export async function deployAmpl(admin: SignerWithAddress) { - const factory = await ethers.getContractFactory('MockAmpl') - const ampl = await upgrades.deployProxy(factory, [admin.address], { - initializer: 'initialize(address)', - }) - await ampl.connect(admin).setMonetaryPolicy(admin.address) - const amplInitialSupply = await ampl.balanceOf(admin.address) - return { ampl, amplInitialSupply } -} - export async function deployMist(admin: SignerWithAddress) { const factory = await ethers.getContractFactory('Alchemist') From f8a3b16e0f73a7d357a40c0d19e5b3cdee1650ce Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 01:05:23 +0100 Subject: [PATCH 05/10] add missing mock contracts --- contracts/mock/MockDelegate.sol | 44 ++++++++++++++++++++++++++++++ contracts/mock/MockERC1271.sol | 16 +++++++++++ contracts/mock/MockPowered.sol | 26 ++++++++++++++++++ contracts/mock/MockSmartWallet.sol | 16 +++++++++++ contracts/mock/MockStakeHelper.sol | 33 ++++++++++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 contracts/mock/MockDelegate.sol create mode 100644 contracts/mock/MockERC1271.sol create mode 100644 contracts/mock/MockPowered.sol create mode 100644 contracts/mock/MockSmartWallet.sol create mode 100644 contracts/mock/MockStakeHelper.sol diff --git a/contracts/mock/MockDelegate.sol b/contracts/mock/MockDelegate.sol new file mode 100644 index 0000000..a04d133 --- /dev/null +++ b/contracts/mock/MockDelegate.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.7.6; + +import {IRageQuit, IUniversalVault} from "../crucible/Crucible.sol"; + +contract MockDelegate is IRageQuit { + enum DelegateType {Succeed, Revert, RevertWithMessage, OOG} + + DelegateType private _delegateType; + + function setDelegateType(DelegateType delegateType) external { + _delegateType = delegateType; + } + + function rageQuit() external view override { + if (_delegateType == DelegateType.Succeed) { + return; + } else if (_delegateType == DelegateType.Revert) { + revert(); + } else if (_delegateType == DelegateType.RevertWithMessage) { + require(false, "MockDelegate: revert with message"); + } else if (_delegateType == DelegateType.OOG) { + while (true) {} + } + } + + function lock( + address vault, + address token, + uint256 amount, + bytes memory permission + ) external { + IUniversalVault(vault).lock(token, amount, permission); + } + + function unlock( + address vault, + address token, + uint256 amount, + bytes memory permission + ) external { + IUniversalVault(vault).unlock(token, amount, permission); + } +} diff --git a/contracts/mock/MockERC1271.sol b/contracts/mock/MockERC1271.sol new file mode 100644 index 0000000..2bc041b --- /dev/null +++ b/contracts/mock/MockERC1271.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.7.6; + +import {ERC1271} from "../crucible/ERC1271.sol"; + +contract MockERC1271 is ERC1271 { + address public owner; + + constructor(address _owner) { + owner = _owner; + } + + function _getOwner() internal view override returns (address) { + return owner; + } +} diff --git a/contracts/mock/MockPowered.sol b/contracts/mock/MockPowered.sol new file mode 100644 index 0000000..a581ec0 --- /dev/null +++ b/contracts/mock/MockPowered.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.7.6; + +import {Powered} from "../aludel/Powered.sol"; + +contract MockPowered is Powered { + constructor(address powerSwitch) { + Powered._setPowerSwitch(powerSwitch); + } + + function onlyOnlineCall() public view onlyOnline { + return; + } + + function onlyOfflineCall() public view onlyOffline { + return; + } + + function notShutdownCall() public view notShutdown { + return; + } + + function onlyShutdownCall() public view onlyShutdown { + return; + } +} diff --git a/contracts/mock/MockSmartWallet.sol b/contracts/mock/MockSmartWallet.sol new file mode 100644 index 0000000..af356ee --- /dev/null +++ b/contracts/mock/MockSmartWallet.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.7.6; + +import {ERC1271} from "../crucible/ERC1271.sol"; + +contract MockSmartWallet is ERC1271 { + address private _owner; + + constructor(address owner) { + _owner = owner; + } + + function _getOwner() internal view override returns (address) { + return _owner; + } +} diff --git a/contracts/mock/MockStakeHelper.sol b/contracts/mock/MockStakeHelper.sol new file mode 100644 index 0000000..8737522 --- /dev/null +++ b/contracts/mock/MockStakeHelper.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.7.6; +pragma abicoder v2; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {Aludel} from "../aludel/Aludel.sol"; +// import {IFactory} from "../Factory/IFactory.sol"; +// import {IUniversalVault} from "../UniversalVault.sol"; + +contract MockStakeHelper { + function flashStake( + address geyser, + address vault, + uint256 amount, + bytes calldata lockPermission, + bytes calldata unstakePermission + ) external { + Aludel(geyser).stake(vault, amount, lockPermission); + Aludel(geyser).unstakeAndClaim(vault, amount, unstakePermission); + } + + function stakeBatch( + address[] calldata geysers, + address[] calldata vaults, + uint256[] calldata amounts, + bytes[] calldata permissions + ) external { + for (uint256 index = 0; index < vaults.length; index++) { + Aludel(geysers[index]).stake(vaults[index], amounts[index], permissions[index]); + } + } +} From cf7f4463bc2890efd2ead506286aca0adf6c8fef Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 01:07:53 +0100 Subject: [PATCH 06/10] oz/hardhat-upgrades is not longed used --- test/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils.ts b/test/utils.ts index 9f20f98..4482c23 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,7 +1,7 @@ import { TypedDataField } from '@ethersproject/abstract-signer' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' import { BigNumber, BigNumberish, BytesLike, Contract, Signer, Wallet } from 'ethers' -import { ethers, network, upgrades } from 'hardhat' +import { ethers, network } from 'hardhat' import '@openzeppelin/hardhat-upgrades' import { parseEther } from 'ethers/lib/utils' From aa5b63fe076796869f356661aade767409cf1c65 Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 01:16:02 +0100 Subject: [PATCH 07/10] set mist as reward token and remove rebasing tests also fix revert message check --- test/Geyser.ts | 83 +++++--------------------------------------------- test/utils.ts | 10 ------ 2 files changed, 8 insertions(+), 85 deletions(-) diff --git a/test/Geyser.ts b/test/Geyser.ts index 40e40f8..2d1b9b9 100644 --- a/test/Geyser.ts +++ b/test/Geyser.ts @@ -4,13 +4,12 @@ import { BigNumber, BigNumberish, Contract, Wallet } from 'ethers' import { ethers, network } from 'hardhat' import { createInstance, - deployAmpl, deployContract, deployAludel, getTimestamp, increaseTime, - invokeRebase, signPermission, + deployMist, } from './utils' /* @@ -134,8 +133,9 @@ describe('Aludel', function () { vaultFactory = await deployContract('CrucibleFactory', [vaultTemplate.address]) // deploy mock tokens - stakingToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) - ;({ ampl: rewardToken, amplInitialSupply } = await deployAmpl(admin)) + stakingToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]); + + ({ mist: rewardToken, initialSupply: amplInitialSupply } = await deployMist(admin)) bonusToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) }) @@ -320,73 +320,6 @@ describe('Aludel', function () { .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(2)) }) }) - describe('with positive rebase of 200%', function () { - beforeEach(async function () { - // rebase of 100 doubles the inital supply - await invokeRebase(rewardToken, 100, admin) - await rewardToken.connect(admin).approve(geyser.address, amplInitialSupply) - }) - it('should succeed', async function () { - await geyser.connect(admin).fund(amplInitialSupply, YEAR) - }) - it('should update state correctly', async function () { - await geyser.connect(admin).fund(amplInitialSupply, YEAR) - - const data = await geyser.getAludelData() - - expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) - expect(data.rewardSchedules.length).to.eq(2) - expect(data.rewardSchedules[0].duration).to.eq(YEAR) - expect(data.rewardSchedules[0].start).to.eq((await getTimestamp()) - 3) - expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) - expect(data.rewardSchedules[1].duration).to.eq(YEAR) - expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) - expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) - }) - it('should emit event', async function () { - await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)) - .to.emit(geyser, 'AludelFunded') - .withArgs(amplInitialSupply, YEAR) - }) - it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fund(amplInitialSupply, YEAR)) - .to.emit(rewardToken, 'Transfer') - .withArgs(admin.address, rewardPool.address, amplInitialSupply) - }) - }) - describe('with negative rebase of 50%', function () { - beforeEach(async function () { - // rebase of -50 halves the inital supply - await invokeRebase(rewardToken, -50, admin) - }) - it('should succeed', async function () { - await geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR) - }) - it('should update state correctly', async function () { - await geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR) - - const data = await geyser.getAludelData() - - expect(data.rewardSharesOutstanding).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI)) - expect(data.rewardSchedules.length).to.eq(2) - expect(data.rewardSchedules[0].duration).to.eq(YEAR) - expect(data.rewardSchedules[0].start).to.eq((await getTimestamp()) - 2) - expect(data.rewardSchedules[0].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) - expect(data.rewardSchedules[1].duration).to.eq(YEAR) - expect(data.rewardSchedules[1].start).to.eq(await getTimestamp()) - expect(data.rewardSchedules[1].shares).to.eq(amplInitialSupply.mul(BASE_SHARES_PER_WEI).div(2)) - }) - it('should emit event', async function () { - await expect(geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR)) - .to.emit(geyser, 'AludelFunded') - .withArgs(amplInitialSupply.div(4), YEAR) - }) - it('should transfer tokens', async function () { - await expect(geyser.connect(admin).fund(amplInitialSupply.div(4), YEAR)) - .to.emit(rewardToken, 'Transfer') - .withArgs(admin.address, rewardPool.address, amplInitialSupply.div(4)) - }) - }) }) describe('after unstake', function () { const stakeAmount = ethers.utils.parseEther('100') @@ -1051,7 +984,7 @@ describe('Aludel', function () { describe('with insufficient balance', function () { it('should fail', async function () { await expect(stake(user, geyser, vault, stakingToken, stakeAmount.mul(2))).to.be.revertedWith( - 'Crucible: insufficient balance', + 'UniversalVault: insufficient balance', ) }) }) @@ -2329,7 +2262,7 @@ describe('Aludel', function () { secondVault.connect(user).rageQuit(geyser.address, stakingToken.address, { gasLimit, }), - ).to.be.revertedWith('Crucible: missing lock') + ).to.be.revertedWith('UniversalVault: missing lock') }) }) describe('when insufficient gas', function () { @@ -2338,7 +2271,7 @@ describe('Aludel', function () { vault.connect(user).rageQuit(geyser.address, stakingToken.address, { gasLimit: await vault.RAGEQUIT_GAS(), }), - ).to.be.revertedWith('Crucible: insufficient gas') + ).to.be.revertedWith('UniversalVault: insufficient gas') }) }) describe('when insufficient gas with multiple stakes', function () { @@ -2355,7 +2288,7 @@ describe('Aludel', function () { vault.connect(user).rageQuit(geyser.address, stakingToken.address, { gasLimit: await vault.RAGEQUIT_GAS(), }), - ).to.be.revertedWith('Crucible: insufficient gas') + ).to.be.revertedWith('UniversalVault: insufficient gas') }) }) describe('when single stake', function () { diff --git a/test/utils.ts b/test/utils.ts index 4482c23..cdff860 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -25,16 +25,6 @@ export async function increaseTime(seconds: number) { } } -// Perc has to be a whole number -export async function invokeRebase(ampl: Contract, perc: number, orchestrator: Signer) { - const PERC_DECIMALS = 2 - const s = await ampl.totalSupply.call() - const ordinate = 10 ** PERC_DECIMALS - const p_ = ethers.BigNumber.from(perc * ordinate).div(100) - const s_ = s.mul(p_).div(ordinate) - await ampl.connect(orchestrator).rebase(1, s_) -} - export async function deployContract(name: string, args: Array = []) { const factory = await ethers.getContractFactory(name) const contract = await factory.deploy(...args) From b3d4a21cd7c11e8d5ac0cb1d09b697470d15150a Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 01:32:24 +0100 Subject: [PATCH 08/10] add @alcuadrado shared-before-each plugin --- test/Geyser.ts | 16 ++-- test/UniversalVault.ts | 8 +- test/shared-before-each/revert-after.ts | 25 +++++++ test/shared-before-each/shared-before-each.ts | 75 +++++++++++++++++++ test/shared-before-each/utils.ts | 33 ++++++++ 5 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 test/shared-before-each/revert-after.ts create mode 100644 test/shared-before-each/shared-before-each.ts create mode 100644 test/shared-before-each/utils.ts diff --git a/test/Geyser.ts b/test/Geyser.ts index 2d1b9b9..46ea7b0 100644 --- a/test/Geyser.ts +++ b/test/Geyser.ts @@ -2,6 +2,8 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-wit import { expect } from 'chai' import { BigNumber, BigNumberish, Contract, Wallet } from 'ethers' import { ethers, network } from 'hardhat' +import { revertAfter } from './shared-before-each/revert-after' +import { sharedBeforeEach } from './shared-before-each/shared-before-each' import { createInstance, deployContract, @@ -114,6 +116,8 @@ describe('Aludel', function () { return stakeDuration >= rewardScaling.time ? baseReward : minReward.add(bonusReward) } + revertAfter(); + before(async function () { // prepare signers accounts = await ethers.getSigners() @@ -123,21 +127,23 @@ describe('Aludel', function () { to: user.address, value: (await accounts[2].getBalance()).mul(9).div(10), }) + }) - beforeEach(async function () { - // deploy dependencies + sharedBeforeEach(async function () { powerSwitchFactory = await deployContract('PowerSwitchFactory') rewardPoolFactory = await deployContract('RewardPoolFactory') vaultTemplate = await deployContract('Crucible') vaultFactory = await deployContract('CrucibleFactory', [vaultTemplate.address]) - + // deploy mock tokens stakingToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]); - + + // unpack object ({ mist: rewardToken, initialSupply: amplInitialSupply } = await deployMist(admin)) + bonusToken = await deployContract('MockERC20', [admin.address, mockTokenSupply]) - }) + }); describe('initialize', function () { describe('when rewardScaling.floor > rewardScaling.ceiling', function () { diff --git a/test/UniversalVault.ts b/test/UniversalVault.ts index 3b83b6c..6865d7f 100644 --- a/test/UniversalVault.ts +++ b/test/UniversalVault.ts @@ -2,6 +2,8 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-wit import { expect } from 'chai' import { Contract, Wallet } from 'ethers' import { ethers } from 'hardhat' +import { revertAfter } from './shared-before-each/revert-after' +import { sharedBeforeEach } from './shared-before-each/shared-before-each' import { createInstance, deployContract, ETHER, signPermission } from './utils' enum DelegateType { @@ -16,6 +18,8 @@ describe('Crucible', function () { let owner: Wallet let factory: Contract, vault: Contract + revertAfter(); + before(async function () { // prepare signers accounts = await ethers.getSigners() @@ -29,7 +33,7 @@ describe('Crucible', function () { }) }) - beforeEach(async function () { + sharedBeforeEach(async function () { // deploy template const template = await deployContract('Crucible') @@ -38,7 +42,7 @@ describe('Crucible', function () { // deploy instance vault = await createInstance('Crucible', factory, owner) - }) + }); describe('nft', function () { it('should succeed', async function () { diff --git a/test/shared-before-each/revert-after.ts b/test/shared-before-each/revert-after.ts new file mode 100644 index 0000000..5611866 --- /dev/null +++ b/test/shared-before-each/revert-after.ts @@ -0,0 +1,25 @@ +import { EthereumProvider } from "hardhat/types"; + +import { wrapWithTitle, takeSnapshot, revert, getProvider } from "./utils"; + +/** + * ThisMocha helper reverts all your state modifications in an `after` hook. + * + * @param title A title that's included in all the hooks that this helper uses. + * @param provider The network provider. + */ +export function revertAfter(title?: string, provider?: EthereumProvider) { + let snapshotId: string | undefined; + before( + wrapWithTitle(title, "resetAfter: taking snapshot"), + async function () { + snapshotId = await takeSnapshot(await getProvider(provider)); + } + ); + + after(wrapWithTitle(title, "resetAfter: reverting state"), async function () { + if (snapshotId !== undefined) { + await revert(await getProvider(provider), snapshotId); + } + }); +} \ No newline at end of file diff --git a/test/shared-before-each/shared-before-each.ts b/test/shared-before-each/shared-before-each.ts new file mode 100644 index 0000000..7fa0873 --- /dev/null +++ b/test/shared-before-each/shared-before-each.ts @@ -0,0 +1,75 @@ +import { EthereumProvider } from "hardhat/types"; + +import { wrapWithTitle, takeSnapshot, revert, getProvider } from "./utils"; + +const SNAPSHOTS: string[] = []; + +/** + * This Mocha helper acts as a `beforeEach`, but executes the initializer + * just once. It internally uses Hardhat Network and Ganache's snapshots + * and revert instead of re-executing the initializer. + * + * Note that after the last test is run, the state doesn't get reverted. + * + * @param title A title that's included in all the hooks that this helper uses. + * @param initializer The initializer to be run before the tests. + * @param provider The network provider. + */ +export function sharedBeforeEach( + title: string, + initializer: Mocha.AsyncFunc, + provider?: EthereumProvider +): void; +export function sharedBeforeEach( + initializer: Mocha.AsyncFunc, + provider?: EthereumProvider +): void; +export function sharedBeforeEach( + titleOrInitializer: string | Mocha.AsyncFunc, + initializerOrProvider?: Mocha.AsyncFunc | EthereumProvider, + optionalProvider?: EthereumProvider +) { + const title = + typeof titleOrInitializer === "string" ? titleOrInitializer : undefined; + + let initializer: Mocha.AsyncFunc; + let maybeProvider: EthereumProvider | undefined; + if (typeof titleOrInitializer === "function") { + initializer = titleOrInitializer; + maybeProvider = initializerOrProvider as EthereumProvider | undefined; + } else { + initializer = initializerOrProvider as Mocha.AsyncFunc; + maybeProvider = optionalProvider; + } + + let initialized = false; + + beforeEach( + wrapWithTitle(title, "Running shared before each or reverting"), + async function () { + const provider = await getProvider(maybeProvider); + if (!initialized) { + const prevSnapshot = SNAPSHOTS.pop(); + if (prevSnapshot !== undefined) { + await revert(provider, prevSnapshot); + SNAPSHOTS.push(await takeSnapshot(provider)); + } + + await initializer.call(this); + + SNAPSHOTS.push(await takeSnapshot(provider)); + initialized = true; + } else { + const snapshotId = SNAPSHOTS.pop()!; + await revert(provider, snapshotId); + SNAPSHOTS.push(await takeSnapshot(provider)); + } + } + ); + + after(async function () { + if (initialized) { + SNAPSHOTS.pop(); + } + }); +} diff --git a/test/shared-before-each/utils.ts b/test/shared-before-each/utils.ts new file mode 100644 index 0000000..bf37992 --- /dev/null +++ b/test/shared-before-each/utils.ts @@ -0,0 +1,33 @@ +import { EthereumProvider } from "hardhat/types"; + +export async function takeSnapshot(provider: EthereumProvider) { + return (await provider.request({ + method: "evm_snapshot", + })) as string; +} + +export async function revert(provider: EthereumProvider, snapshotId: string) { + await provider.request({ + method: "evm_revert", + params: [snapshotId], + }); +} + +export async function getProvider( + provider?: EthereumProvider +): Promise { + if (provider !== undefined) { + return provider; + } + + const hre = await import("hardhat"); + return hre.network.provider; +} + +export function wrapWithTitle(title: string | undefined, str: string) { + if (title === undefined) { + return str; + } + + return `${title} at step "${str}"`; +} From 244429b9d9717fb67bf4bae45fdea708921e7355 Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 09:19:39 +0100 Subject: [PATCH 09/10] remove unsed imports --- contracts/mock/MockStakeHelper.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/mock/MockStakeHelper.sol b/contracts/mock/MockStakeHelper.sol index 8737522..4d5d972 100644 --- a/contracts/mock/MockStakeHelper.sol +++ b/contracts/mock/MockStakeHelper.sol @@ -5,8 +5,6 @@ pragma abicoder v2; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Aludel} from "../aludel/Aludel.sol"; -// import {IFactory} from "../Factory/IFactory.sol"; -// import {IUniversalVault} from "../UniversalVault.sol"; contract MockStakeHelper { function flashStake( From dfeaa883e879cf385648d2f641044646492fe57c Mon Sep 17 00:00:00 2001 From: budi Date: Wed, 14 Jul 2021 09:19:52 +0100 Subject: [PATCH 10/10] remove commented code --- test/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils.ts b/test/utils.ts index cdff860..dc78a1c 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -44,7 +44,7 @@ export async function deployMist(admin: SignerWithAddress) { 1000, BigNumber.from(14).mul(DAY), BigNumber.from(60).mul(DAY), - parseEther("1000"), //parseEther(1000000*10**18), + parseEther("1000"), Math.round(now.getTime() / 1000) )