From 693b58bbda69f1242c8478ae639cad362a3c8c39 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Mon, 14 Oct 2024 07:53:58 -0400 Subject: [PATCH 01/23] corrected test filenames --- .../test/integration/{Account.ts => Account.test.ts} | 0 .../test/integration/{Controller.ts => Controller.test.ts} | 0 .../{Controller_Arbitrum.ts => Controller_Arbitrum.test.ts} | 0 .../test/unit/{Controller.ts => Controller.test.ts} | 0 .../test/unit/{RebalanceConfig.ts => RebalanceConfig.test.ts} | 0 .../perennial-account/test/unit/{Verifier.ts => Verifier.test.ts} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename packages/perennial-account/test/integration/{Account.ts => Account.test.ts} (100%) rename packages/perennial-account/test/integration/{Controller.ts => Controller.test.ts} (100%) rename packages/perennial-account/test/integration/{Controller_Arbitrum.ts => Controller_Arbitrum.test.ts} (100%) rename packages/perennial-account/test/unit/{Controller.ts => Controller.test.ts} (100%) rename packages/perennial-account/test/unit/{RebalanceConfig.ts => RebalanceConfig.test.ts} (100%) rename packages/perennial-account/test/unit/{Verifier.ts => Verifier.test.ts} (100%) diff --git a/packages/perennial-account/test/integration/Account.ts b/packages/perennial-account/test/integration/Account.test.ts similarity index 100% rename from packages/perennial-account/test/integration/Account.ts rename to packages/perennial-account/test/integration/Account.test.ts diff --git a/packages/perennial-account/test/integration/Controller.ts b/packages/perennial-account/test/integration/Controller.test.ts similarity index 100% rename from packages/perennial-account/test/integration/Controller.ts rename to packages/perennial-account/test/integration/Controller.test.ts diff --git a/packages/perennial-account/test/integration/Controller_Arbitrum.ts b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts similarity index 100% rename from packages/perennial-account/test/integration/Controller_Arbitrum.ts rename to packages/perennial-account/test/integration/Controller_Arbitrum.test.ts diff --git a/packages/perennial-account/test/unit/Controller.ts b/packages/perennial-account/test/unit/Controller.test.ts similarity index 100% rename from packages/perennial-account/test/unit/Controller.ts rename to packages/perennial-account/test/unit/Controller.test.ts diff --git a/packages/perennial-account/test/unit/RebalanceConfig.ts b/packages/perennial-account/test/unit/RebalanceConfig.test.ts similarity index 100% rename from packages/perennial-account/test/unit/RebalanceConfig.ts rename to packages/perennial-account/test/unit/RebalanceConfig.test.ts diff --git a/packages/perennial-account/test/unit/Verifier.ts b/packages/perennial-account/test/unit/Verifier.test.ts similarity index 100% rename from packages/perennial-account/test/unit/Verifier.ts rename to packages/perennial-account/test/unit/Verifier.test.ts From 6522ff8260eede4716ae66ee9f2ca891f86c4908 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Mon, 14 Oct 2024 10:44:50 -0400 Subject: [PATCH 02/23] the easy part --- .../contracts/Controller_Optimism.sol | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/perennial-account/contracts/Controller_Optimism.sol diff --git a/packages/perennial-account/contracts/Controller_Optimism.sol b/packages/perennial-account/contracts/Controller_Optimism.sol new file mode 100644 index 000000000..4ab84427b --- /dev/null +++ b/packages/perennial-account/contracts/Controller_Optimism.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { Kept_Optimism, Kept } from "@equilibria/root/attribute/Kept/Kept_Optimism.sol"; +import { UFixed18 } from "@equilibria/root/number/types/UFixed18.sol"; +import { IVerifierBase } from "@equilibria/root/verifier/interfaces/IVerifierBase.sol"; +import { IMarketFactory } from "@equilibria/perennial-v2/contracts/interfaces/IMarketFactory.sol"; +import { Controller_Incentivized } from "./Controller_Incentivized.sol"; + +/// @title Controller_Optimism +/// @notice Controller which compensates keepers for handling or relaying messages on Optimism L2. +contract Controller_Optimism is Controller_Incentivized, Kept_Optimism { + /// @dev Creates instance of Controller which compensates keepers + /// @param implementation Pristine collateral account contract + /// @param marketFactory Market Factory contract + /// @param nonceManager Verifier contract to which nonce and group cancellations are relayed + constructor( + address implementation, + IMarketFactory marketFactory, + IVerifierBase nonceManager + ) Controller_Incentivized(implementation, marketFactory, nonceManager) {} + + /// @dev Use the Kept_Optimism implementation for calculating the dynamic fee + function _calldataFee( + bytes memory applicableCalldata, + UFixed18 multiplierCalldata, + uint256 bufferCalldata + ) internal view override(Kept_Optimism, Kept) returns (UFixed18) { + return Kept_Optimism._calldataFee(applicableCalldata, multiplierCalldata, bufferCalldata); + } + + /// @dev Transfers funds from collateral account to controller, and limits compensation + /// to the user-defined maxFee in the Action message + /// @param amount Calculated keeper fee + /// @param data Encoded address of collateral account and UFixed6 user-specified maximum fee + /// @return raisedKeeperFee Amount pulled from controller to keeper + function _raiseKeeperFee( + UFixed18 amount, + bytes memory data + ) internal override(Controller_Incentivized, Kept) returns (UFixed18 raisedKeeperFee) { + return Controller_Incentivized._raiseKeeperFee(amount, data); + } +} From 78d4fce16c53646fc86155a45b0e7e535b298956 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Mon, 14 Oct 2024 12:40:06 -0400 Subject: [PATCH 03/23] reworked incentivized integration tests --- .../integration/Controller_Arbitrum.test.ts | 990 +----------------- .../Controller_Incentivized.test.ts | 965 +++++++++++++++++ 2 files changed, 1022 insertions(+), 933 deletions(-) create mode 100644 packages/perennial-account/test/integration/Controller_Incentivized.test.ts diff --git a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts index 7cbe8f123..9677423f2 100644 --- a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts @@ -1,28 +1,8 @@ -import { expect } from 'chai' -import HRE from 'hardhat' -import { Address } from 'hardhat-deploy/dist/types' -import { BigNumber, constants, utils } from 'ethers' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { smock } from '@defi-wonderland/smock' -import { advanceBlock, currentBlockTimestamp } from '../../../common/testutil/time' -import { getEventArguments } from '../../../common/testutil/transaction' - -import { parse6decimal } from '../../../common/testutil/types' -import { - Account, - Account__factory, - AccountVerifier__factory, - ArbGasInfo, - Controller_Arbitrum, - IAccount, - IAccountVerifier, - IERC20Metadata, - IMarket, - IMarketFactory, -} from '../../types/generated' +import { use } from 'chai' +import { CallOverrides, Signer } from 'ethers' +import { ArbGasInfo } from '../../../types/generated' import { createMarketBTC, createMarketETH, @@ -32,916 +12,60 @@ import { fundWalletUSDC, getStablecoins, } from '../helpers/arbitrumHelpers' -import { - signDeployAccount, - signMarketTransfer, - signRebalanceConfigChange, - signRelayedAccessUpdateBatch, - signRelayedGroupCancellation, - signRelayedNonceCancellation, - signRelayedOperatorUpdate, - signRelayedSignerUpdate, - signWithdrawal, -} from '../helpers/erc712' -import { advanceToPrice } from '../helpers/setupHelpers' -import { - signAccessUpdateBatch, - signGroupCancellation, - signCommon as signNonceCancellation, - signOperatorUpdate, - signSignerUpdate, -} from '@equilibria/perennial-v2-verifier/test/helpers/erc712' -import { Verifier, Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' -import { IVerifier__factory } from '@equilibria/perennial-v2/types/generated' -import { IKeeperOracle, IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' - -const { ethers } = HRE - -const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' // price feed used for keeper compensation -const DEFAULT_MAX_FEE = parse6decimal('0.5') - -// hack around issues estimating gas for instrumented contracts when running tests under coverage -const TX_OVERRIDES = { gasLimit: 3_000_000, maxPriorityFeePerGas: 0, maxFeePerGas: 100_000_000 } - -describe('Controller_Arbitrum', () => { - let dsu: IERC20Metadata - let usdc: IERC20Metadata - let controller: Controller_Arbitrum - let accountVerifier: IAccountVerifier - let oracleFactory: IOracleFactory - let pythOracleFactory: PythFactory - let marketFactory: IMarketFactory - let market: IMarket - let btcMarket: IMarket - let ethKeeperOracle: IKeeperOracle - let btcKeeperOracle: IKeeperOracle - let owner: SignerWithAddress - let userA: SignerWithAddress - let userB: SignerWithAddress - let userC: SignerWithAddress - let keeper: SignerWithAddress - let receiver: SignerWithAddress - let lastNonce = 0 - let currentTime: BigNumber - let keeperBalanceBefore: BigNumber - let keeperEthBalanceBefore: BigNumber - - // create a default action for the specified user with reasonable fee and expiry - function createAction( - userAddress: Address, - signerAddress = userAddress, - maxFee = DEFAULT_MAX_FEE, - expiresInSeconds = 45, - ) { - return { - action: { - maxFee: maxFee, - common: { - account: userAddress, - signer: signerAddress, - domain: controller.address, - nonce: nextNonce(), - group: 0, - expiry: currentTime.add(expiresInSeconds), - }, - }, - } - } - - // deploys and funds a collateral account - async function createCollateralAccount(user: SignerWithAddress, amount: BigNumber): Promise { - const accountAddress = await controller.getAccountAddress(user.address) - await usdc.connect(userA).transfer(accountAddress, amount, TX_OVERRIDES) - const deployAccountMessage = { - ...createAction(user.address, user.address), - } - const signatureCreate = await signDeployAccount(user, accountVerifier, deployAccountMessage) - const tx = await controller - .connect(keeper) - .deployAccountWithSignature(deployAccountMessage, signatureCreate, TX_OVERRIDES) - - // verify the address from event arguments - const creationArgs = await getEventArguments(tx, 'AccountDeployed') - expect(creationArgs.account).to.equal(accountAddress) - - // approve the collateral account as operator - await marketFactory.connect(user).updateOperator(accountAddress, true, TX_OVERRIDES) - - return Account__factory.connect(accountAddress, user) - } - - // funds specified wallet with 50k USDC - async function fundWallet(wallet: SignerWithAddress): Promise { - await fundWalletUSDC(wallet, parse6decimal('50000'), { maxFeePerGas: 100000000 }) - } - - async function checkCompensation(priceCommitments = 0) { - const keeperFeesPaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) - let keeperEthSpentOnGas = keeperEthBalanceBefore.sub(await keeper.getBalance()) - - // if TXes in test required outside price commitments, compensate the keeper for them - keeperEthSpentOnGas = keeperEthSpentOnGas.add(utils.parseEther('0.0000644306').mul(priceCommitments)) - - // cost of transaction - const keeperGasCostInUSD = keeperEthSpentOnGas.mul(3413) - // keeper should be compensated between 100-125% of actual gas cost - expect(keeperFeesPaid).to.be.within(keeperGasCostInUSD, keeperGasCostInUSD.mul(125).div(100)) - } - - // create a serial nonce for testing purposes; real users may choose a nonce however they please - function nextNonce(): BigNumber { - lastNonce += 1 - return BigNumber.from(lastNonce) - } - - // deposit from the collateral account to the ETH market - async function deposit(amount: BigNumber, account: IAccount) { - // sign the message - const marketTransferMessage = { - market: market.address, - amount: amount, - ...createAction(userA.address, userA.address), - } - const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) - - // perform transfer - await expect(controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(account.address, market.address, anyValue) // scale to token precision - .to.emit(market, 'OrderCreated') - .withArgs(userA.address, anyValue, anyValue, constants.AddressZero, constants.AddressZero, constants.AddressZero) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - } - - const fixture = async () => { - // deploy the protocol - ;[owner, userA, userB, userC, keeper, receiver] = await ethers.getSigners() - ;[oracleFactory, marketFactory, pythOracleFactory] = await createFactories(owner) - ;[dsu, usdc] = await getStablecoins(owner) - ;[market, , ethKeeperOracle] = await createMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) - ;[btcMarket, , btcKeeperOracle] = await createMarketBTC( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - TX_OVERRIDES, - ) - await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES) - await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('57575.464'), TX_OVERRIDES) - - await dsu.connect(userA).approve(market.address, constants.MaxUint256, { maxFeePerGas: 100000000 }) - - // set up users and deploy artifacts - const keepConfig = { - multiplierBase: ethers.utils.parseEther('1'), - bufferBase: 275_000, // buffer for handling the keeper fee - multiplierCalldata: ethers.utils.parseEther('1'), - bufferCalldata: 0, - } - const keepConfigBuffered = { - multiplierBase: ethers.utils.parseEther('1.08'), - bufferBase: 1_500_000, // for price commitment - multiplierCalldata: ethers.utils.parseEther('1.08'), - bufferCalldata: 35_200, - } - const keepConfigWithdrawal = { - multiplierBase: ethers.utils.parseEther('1.05'), - bufferBase: 1_500_000, - multiplierCalldata: ethers.utils.parseEther('1.05'), - bufferCalldata: 35_200, - } - const marketVerifier = IVerifier__factory.connect(await marketFactory.verifier(), owner) - controller = await deployControllerArbitrum(owner, marketFactory, marketVerifier, { - maxFeePerGas: 100000000, - }) - accountVerifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address, { - maxFeePerGas: 100000000, - }) - // chainlink feed is used by Kept for keeper compensation - const KeepConfig = '(uint256,uint256,uint256,uint256)' - await controller[`initialize(address,address,${KeepConfig},${KeepConfig},${KeepConfig})`]( - accountVerifier.address, - CHAINLINK_ETH_USD_FEED, - keepConfig, - keepConfigBuffered, - keepConfigWithdrawal, - ) - // fund userA - await fundWallet(userA) +import { DeploymentVars, RunCollateralAccountTests } from './Controller_Incentivized.test' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/generated' + +use(smock.matchers) + +async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { + const [oracleFactory, marketFactory, pythOracleFactory] = await createFactories(owner) + const [dsu, usdc] = await getStablecoins(owner) + const [ethMarket, , ethKeeperOracle] = await createMarketETH( + owner, + oracleFactory, + pythOracleFactory, + marketFactory, + dsu, + ) + const [btcMarket, , btcKeeperOracle] = await createMarketBTC( + owner, + oracleFactory, + pythOracleFactory, + marketFactory, + dsu, + overrides, + ) + return { + dsu, + usdc, + oracleFactory, + pythOracleFactory, + marketFactory, + ethMarket, + btcMarket, + ethKeeperOracle, + btcKeeperOracle, + fundWalletDSU, + fundWalletUSDC, } - - before(async () => { - // touch the provider, such that smock doesn't error out running a single test - await advanceBlock() - // Hardhat fork does not support Arbitrum built-ins; Kept produces "invalid opcode" error without this - await smock.fake('ArbGasInfo', { - address: '0x000000000000000000000000000000000000006C', - }) - }) - - beforeEach(async () => { - // update the timestamp used for calculating expiry and adjusting oracle price - currentTime = BigNumber.from(await currentBlockTimestamp()) - await loadFixture(fixture) - - // set a realistic base gas fee - await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x5F5E100']) // 0.1 gwei - - keeperBalanceBefore = await dsu.balanceOf(keeper.address) - keeperEthBalanceBefore = await keeper.getBalance() - currentTime = BigNumber.from(await currentBlockTimestamp()) - }) - - afterEach(async () => { - // ensure controller has no funds at rest - expect(await dsu.balanceOf(controller.address)).to.equal(0) - - // reset to avoid impact to setup and other tests - await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x1']) - }) - - describe('#deployment', () => { - let accountAddressA: Address - - // fund the account with 15k USDC - beforeEach(async () => { - accountAddressA = await controller.getAccountAddress(userA.address) - }) - - it('can create an account', async () => { - // pre-fund the address where the account will be deployed - await usdc.connect(userA).transfer(accountAddressA, parse6decimal('15000'), TX_OVERRIDES) - - // sign a message to deploy the account - const deployAccountMessage = { - ...createAction(userA.address, userA.address), - } - const signature = await signDeployAccount(userA, accountVerifier, deployAccountMessage) - - // keeper executes deployment of the account and is compensated - await expect(controller.connect(keeper).deployAccountWithSignature(deployAccountMessage, signature, TX_OVERRIDES)) - .to.emit(controller, 'AccountDeployed') - .withArgs(userA.address, accountAddressA) - - await checkCompensation() - }) - - it('keeper fee is limited by maxFee', async () => { - // pre-fund the address where the account will be deployed - await usdc.connect(userA).transfer(accountAddressA, parse6decimal('15000'), TX_OVERRIDES) - - // sign a message with maxFee smaller than the calculated keeper fee (~0.0033215) - const maxFee = parse6decimal('0.0789') - const deployAccountMessage = { - ...createAction(userA.address, userA.address, maxFee), - } - const signature = await signDeployAccount(userA, accountVerifier, deployAccountMessage) - - // keeper executes deployment of the account and is compensated - await expect(controller.connect(keeper).deployAccountWithSignature(deployAccountMessage, signature, TX_OVERRIDES)) - .to.emit(controller, 'AccountDeployed') - .withArgs(userA.address, accountAddressA) - - const keeperFeePaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) - expect(keeperFeePaid).to.equal(maxFee.mul(1e12)) // convert from 6- to 18- decimal - }) - - it('reverts if keeper cannot be compensated', async () => { - // ensure the account is empty - expect(await dsu.balanceOf(keeper.address)).to.equal(0) - expect(await usdc.balanceOf(keeper.address)).to.equal(0) - - // sign a message to deploy the account - const deployAccountMessage = { - ...createAction(userA.address, userA.address), - } - - // ensure the request fails - const signature = await signDeployAccount(userA, accountVerifier, deployAccountMessage) - await expect( - controller - .connect(keeper) - .deployAccountWithSignature(deployAccountMessage, signature, { maxFeePerGas: 100000000 }), - ).to.be.reverted - }) +} + +async function deployInstance( + owner: SignerWithAddress, + marketFactory: IMarketFactory, + relayVerifier: IVerifier, + overrides?: CallOverrides, +): Promise { + return deployControllerArbitrum(owner, marketFactory, relayVerifier, overrides) +} + +async function mockGasInfo() { + // Hardhat fork does not support Arbitrum built-ins; Kept produces "invalid opcode" error without this + const gasInfo = await smock.fake('ArbGasInfo', { + address: '0x000000000000000000000000000000000000006C', }) + gasInfo.getL1BaseFeeEstimate.returns(0) +} - describe('#transfer', async () => { - const INITIAL_DEPOSIT_6 = parse6decimal('13000') - let accountA: Account - - beforeEach(async () => { - // deploy collateral account for userA - accountA = await createCollateralAccount(userA, INITIAL_DEPOSIT_6) - }) - - it('collects fee for depositing some funds to market', async () => { - // sign a message to deposit 6k from the collateral account to the market - const transferAmount = parse6decimal('6000') - const marketTransferMessage = { - market: market.address, - amount: transferAmount, - ...createAction(userA.address), - } - const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) - - // perform transfer - await expect( - controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ) - .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, market.address, transferAmount.mul(1e12)) // scale to token precision - .to.emit(market, 'OrderCreated') - .withArgs( - userA.address, - anyValue, - anyValue, - constants.AddressZero, - constants.AddressZero, - constants.AddressZero, - ) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - expect((await market.locals(userA.address)).collateral).to.equal(transferAmount) - - await checkCompensation(1) - }) - - it('collects fee for withdrawing some funds from market', async () => { - // user deposits collateral to the market - await deposit(parse6decimal('12000'), accountA) - expect((await market.locals(userA.address)).collateral).to.equal(parse6decimal('12000')) - - // sign a message to make a partial withdrawal - const withdrawal = parse6decimal('-2000') - const marketTransferMessage = { - market: market.address, - amount: withdrawal, - ...createAction(userA.address, userA.address), - } - const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) - - // perform transfer - await expect( - controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ) - .to.emit(dsu, 'Transfer') - .withArgs(market.address, accountA.address, withdrawal.mul(-1e12)) // scale to token precision - .to.emit(market, 'OrderCreated') - .withArgs( - userA.address, - anyValue, - anyValue, - constants.AddressZero, - constants.AddressZero, - constants.AddressZero, - ) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - expect((await market.locals(userA.address)).collateral).to.equal(parse6decimal('10000')) // 12k-2k - - await checkCompensation(2) - }) - - it('collects fee for withdrawing native deposit from market', async () => { - // user directly deposits collateral to the market - const depositAmount = parse6decimal('13000') - await fundWalletDSU(userA, depositAmount.mul(1e12), TX_OVERRIDES) - await market - .connect(userA) - ['update(address,uint256,uint256,uint256,int256,bool)']( - userA.address, - constants.MaxUint256, - constants.MaxUint256, - constants.MaxUint256, - depositAmount, - false, - { maxFeePerGas: 150000000 }, - ) - expect((await market.locals(userA.address)).collateral).to.equal(depositAmount) - - // sign a message to withdraw everything from the market back into the collateral account - const marketTransferMessage = { - market: market.address, - amount: constants.MinInt256, - ...createAction(userA.address, userA.address), - } - const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) - - // perform transfer - await expect( - controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ) - .to.emit(dsu, 'Transfer') - .withArgs(market.address, accountA.address, depositAmount.mul(1e12)) // scale to token precision - .to.emit(market, 'OrderCreated') - .withArgs( - userA.address, - anyValue, - anyValue, - constants.AddressZero, - constants.AddressZero, - constants.AddressZero, - ) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - expect((await market.locals(userA.address)).collateral).to.equal(0) - - await checkCompensation(1) - }) - - it('collects fee for withdrawing funds into empty collateral account', async () => { - // deposit 12k - await deposit(parse6decimal('12000'), accountA) - // withdraw dust so it cannot be used to pay the keeper - await accountA.withdraw(constants.MaxUint256, true, TX_OVERRIDES) - expect(await dsu.balanceOf(accountA.address)).to.equal(0) - - // sign a message to withdraw 2k from the market back into the collateral account - const withdrawal = parse6decimal('-2000') - const marketTransferMessage = { - market: market.address, - amount: withdrawal, - ...createAction(userA.address, userA.address), - } - const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) - - // perform transfer - await expect( - controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ) - .to.emit(dsu, 'Transfer') - .withArgs(market.address, accountA.address, anyValue) - .to.emit(market, 'OrderCreated') - .withArgs( - userA.address, - anyValue, - anyValue, - constants.AddressZero, - constants.AddressZero, - constants.AddressZero, - ) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - expect((await market.locals(userA.address)).collateral).to.be.within( - parse6decimal('9999'), - parse6decimal('10000'), - ) // 12k-2k - - await checkCompensation(2) - }) - }) - - describe('#rebalance', async () => { - let accountA: Account - - beforeEach(async () => { - accountA = await createCollateralAccount(userA, parse6decimal('10005')) - }) - - it('collects fee for changing rebalance configuration', async () => { - // sign message to create a new group - const message = { - group: 5, - markets: [market.address], - configs: [{ target: parse6decimal('1'), threshold: parse6decimal('0.0901') }], - maxFee: DEFAULT_MAX_FEE, - ...(await createAction(userA.address)), - } - const signature = await signRebalanceConfigChange(userA, accountVerifier, message) - - // create the group - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)) - .to.emit(controller, 'RebalanceMarketConfigured') - .withArgs(userA.address, message.group, market.address, message.configs[0]) - .to.emit(controller, 'RebalanceGroupConfigured') - .withArgs(userA.address, message.group, 1) - - // ensure keeper was compensated - await checkCompensation() - }) - - it('collects fee for rebalancing a group', async () => { - const ethMarket = market - - // create a new group with two markets - const message = { - group: 4, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, - { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, - ], - maxFee: DEFAULT_MAX_FEE, - ...(await createAction(userA.address)), - } - const signature = await signRebalanceConfigChange(userA, accountVerifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)).to - .not.be.reverted - - // transfer all collateral to ethMarket - await deposit(parse6decimal('10000'), accountA) - expect((await ethMarket.locals(userA.address)).collateral).to.equal(parse6decimal('10000')) - expect((await btcMarket.locals(userA.address)).collateral).to.equal(0) - - // rebalance the group - await expect(controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(ethMarket.address, accountA.address, utils.parseEther('5000')) - .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, btcMarket.address, utils.parseEther('5000')) - .to.emit(controller, 'GroupRebalanced') - .withArgs(userA.address, 4) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - - // confirm keeper earned their fee - await checkCompensation(2) - }) - - it('honors max rebalance fee when rebalancing a group', async () => { - const ethMarket = market - - // create a new group with two markets and a maxFee smaller than the actual fee - const message = { - group: 4, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('0.75'), threshold: parse6decimal('0.06') }, - { target: parse6decimal('0.25'), threshold: parse6decimal('0.06') }, - ], - maxFee: parse6decimal('0.00923'), - ...(await createAction(userA.address)), - } - const signature = await signRebalanceConfigChange(userA, accountVerifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)).to - .not.be.reverted - - // transfer some collateral to ethMarket and record keeper balance after account creation - await deposit(parse6decimal('5000'), accountA) - const keeperBalanceBefore = await dsu.balanceOf(keeper.address) - - // rebalance the group - await expect(controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(ethMarket.address, accountA.address, utils.parseEther('1250')) - .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, btcMarket.address, utils.parseEther('1250')) - .to.emit(controller, 'GroupRebalanced') - .withArgs(userA.address, 4) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - - // confirm keeper fee was limited as configured - const keeperFeePaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) - expect(keeperFeePaid).to.equal(utils.parseEther('0.00923')) - }) - - it('cannot award more keeper fees than collateral rebalanced', async () => { - const ethMarket = market - - // create a new group with two markets - const message = { - group: 4, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, - { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, - ], - maxFee: DEFAULT_MAX_FEE, - ...(await createAction(userA.address)), - } - const signature = await signRebalanceConfigChange(userA, accountVerifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)).to - .not.be.reverted - - let dustAmount = parse6decimal('0.000001') - await dsu.connect(keeper).approve(ethMarket.address, dustAmount.mul(1e12), TX_OVERRIDES) - - // keeper dusts one of the markets - await ethMarket - .connect(keeper) - ['update(address,uint256,uint256,uint256,int256,bool)']( - userA.address, - constants.MaxUint256, - constants.MaxUint256, - constants.MaxUint256, - dustAmount, - false, - { maxFeePerGas: 150000000 }, - ) - expect((await ethMarket.locals(userA.address)).collateral).to.equal(dustAmount) - - // keeper cannot rebalance because dust did not exceed maxFee - await expect( - controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES), - ).to.be.revertedWithCustomError(controller, 'ControllerGroupBalancedError') - - // keeper dusts the other market, such that target is nonzero, and percentage exceeded - dustAmount = parse6decimal('0.000003') - await dsu.connect(keeper).approve(btcMarket.address, dustAmount.mul(1e12), TX_OVERRIDES) - await btcMarket - .connect(keeper) - ['update(address,uint256,uint256,uint256,int256,bool)']( - userA.address, - constants.MaxUint256, - constants.MaxUint256, - constants.MaxUint256, - dustAmount, - false, - { maxFeePerGas: 150000000 }, - ) - expect((await btcMarket.locals(userA.address)).collateral).to.equal(dustAmount) - - // keeper still cannot rebalance because dust did not exceed maxFee - await expect( - controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES), - ).to.be.revertedWithCustomError(controller, 'ControllerGroupBalancedError') - }) - }) - - describe('#withdrawal', async () => { - let accountA: Account - let userBalanceBefore: BigNumber - - beforeEach(async () => { - // deploy collateral account for userA - accountA = await createCollateralAccount(userA, parse6decimal('17000')) - userBalanceBefore = await usdc.balanceOf(userA.address) - }) - - afterEach(async () => { - // confirm keeper earned their fee - await checkCompensation(1) - }) - - it('collects fee for partial withdrawal from a delegated signer', async () => { - // configure userB as delegated signer - await marketFactory.connect(userA).updateSigner(userB.address, true, TX_OVERRIDES) - - // delegate signs message for partial withdrawal - const withdrawalAmount = parse6decimal('7000') - const withdrawalMessage = { - amount: withdrawalAmount, - unwrap: true, - ...createAction(userA.address, userB.address), - } - const signature = await signWithdrawal(userB, accountVerifier, withdrawalMessage) - - // perform withdrawal and check balance - await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature, TX_OVERRIDES)) - .to.emit(usdc, 'Transfer') - .withArgs(accountA.address, userA.address, anyValue) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - - // confirm userA withdrew their funds and keeper fee was paid from the collateral account - expect(await usdc.balanceOf(accountA.address)).to.be.within(parse6decimal('9999'), parse6decimal('10000')) - expect(await usdc.balanceOf(userA.address)).to.equal(userBalanceBefore.add(withdrawalAmount)) - }) - - it('collects fee for full withdrawal', async () => { - // sign a message to withdraw all funds from the account - const withdrawalMessage = { - amount: constants.MaxUint256, - unwrap: true, - ...createAction(userA.address, userA.address), - } - const signature = await signWithdrawal(userA, accountVerifier, withdrawalMessage) - - // perform withdrawal and check balances - await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature, TX_OVERRIDES)) - .to.emit(usdc, 'Transfer') - .withArgs(accountA.address, userA.address, anyValue) - .to.emit(controller, 'KeeperCall') - .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) - - // collateral account should be empty - expect(await dsu.balanceOf(accountA.address)).to.equal(0) - expect(await usdc.balanceOf(accountA.address)).to.equal(0) - - // user should have their initial balance, plus what was in their collateral account, minus keeper fees - expect(await usdc.balanceOf(userA.address)).to.be.within(parse6decimal('49999'), parse6decimal('50000')) - }) - }) - - describe('#relay', async () => { - let downstreamVerifier: Verifier - - function createCommon(domain: Address) { - return { - common: { - account: userA.address, - signer: userA.address, - domain: domain, - nonce: nextNonce(), - group: 0, - expiry: currentTime.add(60), - }, - } - } - - beforeEach(async () => { - await createCollateralAccount(userA, parse6decimal('6')) - downstreamVerifier = Verifier__factory.connect(await marketFactory.verifier(), owner) - downstreamVerifier.initialize(marketFactory.address, TX_OVERRIDES) - }) - - afterEach(async () => { - // confirm keeper earned their fee - await checkCompensation() - }) - - it('relays nonce cancellation messages', async () => { - // confirm nonce was not already cancelled - const nonce = 7 - expect(await downstreamVerifier.nonces(userA.address, nonce)).to.eq(false) - - // create and sign the inner message - const nonceCancellation = { - account: userA.address, - signer: userA.address, - domain: downstreamVerifier.address, - nonce: nonce, - group: 0, - expiry: currentTime.add(60), - } - const innerSignature = await signNonceCancellation(userA, downstreamVerifier, nonceCancellation) - - // create and sign the outer message - const relayedNonceCancellation = { - nonceCancellation: nonceCancellation, - ...createAction(userA.address, userA.address), - } - const outerSignature = await signRelayedNonceCancellation(userA, accountVerifier, relayedNonceCancellation) - - // perform the action - await expect( - controller - .connect(keeper) - .relayNonceCancellation(relayedNonceCancellation, outerSignature, innerSignature, TX_OVERRIDES), - ) - .to.emit(downstreamVerifier, 'NonceCancelled') - .withArgs(userA.address, nonce) - .to.emit(accountVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedNonceCancellation.action.common.nonce) - - // confirm nonce is now cancelled - expect(await downstreamVerifier.nonces(userA.address, nonce)).to.eq(true) - }) - - it('relays group cancellation messages', async () => { - // confirm group was not already cancelled - const group = 7 - expect(await downstreamVerifier.groups(userA.address, group)).to.eq(false) - - // create and sign the inner message - const groupCancellation = { - group: group, - ...createCommon(downstreamVerifier.address), - } - const innerSignature = await signGroupCancellation(userA, downstreamVerifier, groupCancellation) - - // create and sign the outer message - const relayedGroupCancellation = { - groupCancellation: groupCancellation, - ...createAction(userA.address, userA.address), - } - const outerSignature = await signRelayedGroupCancellation(userA, accountVerifier, relayedGroupCancellation) - - // perform the action - await expect( - controller - .connect(keeper) - .relayGroupCancellation(relayedGroupCancellation, outerSignature, innerSignature, TX_OVERRIDES), - ) - .to.emit(downstreamVerifier, 'GroupCancelled') - .withArgs(userA.address, group) - .to.emit(downstreamVerifier, 'NonceCancelled') - .withArgs(userA.address, groupCancellation.common.nonce) - .to.emit(accountVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedGroupCancellation.action.common.nonce) - - // confirm group is now cancelled - expect(await downstreamVerifier.groups(userA.address, group)).to.eq(true) - }) - - it('relays operator update messages', async () => { - // confirm userB is not already an operator - expect(await marketFactory.operators(userA.address, userB.address)).to.be.false - - // create and sign the inner message - const operatorUpdate = { - access: { - accessor: userB.address, - approved: true, - }, - ...createCommon(marketFactory.address), - } - const innerSignature = await signOperatorUpdate(userA, downstreamVerifier, operatorUpdate) - - // create and sign the outer message - const relayedOperatorUpdateMessage = { - operatorUpdate: operatorUpdate, - ...createAction(userA.address, userA.address), - } - const outerSignature = await signRelayedOperatorUpdate(userA, accountVerifier, relayedOperatorUpdateMessage) - - // perform the action - await expect( - controller - .connect(keeper) - .relayOperatorUpdate(relayedOperatorUpdateMessage, outerSignature, innerSignature, TX_OVERRIDES), - ) - .to.emit(marketFactory, 'OperatorUpdated') - .withArgs(userA.address, userB.address, true) - .to.emit(downstreamVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedOperatorUpdateMessage.operatorUpdate.common.nonce) - .to.emit(accountVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedOperatorUpdateMessage.action.common.nonce) - - // confirm userB is now an operator - expect(await marketFactory.operators(userA.address, userB.address)).to.be.true - }) - - it('relays signer update messages', async () => { - // confirm userB is not already a delegated signer - expect(await marketFactory.signers(userA.address, userB.address)).to.be.false - - // create and sign the inner message - const signerUpdate = { - access: { - accessor: userB.address, - approved: true, - }, - ...createCommon(marketFactory.address), - } - const innerSignature = await signSignerUpdate(userA, downstreamVerifier, signerUpdate) - - // create and sign the outer message - const relayedSignerUpdateMessage = { - signerUpdate: signerUpdate, - ...createAction(userA.address, userA.address), - } - const outerSignature = await signRelayedSignerUpdate(userA, accountVerifier, relayedSignerUpdateMessage) - - // perform the action - await expect( - controller - .connect(keeper) - .relaySignerUpdate(relayedSignerUpdateMessage, outerSignature, innerSignature, TX_OVERRIDES), - ) - .to.emit(marketFactory, 'SignerUpdated') - .withArgs(userA.address, userB.address, true) - .to.emit(downstreamVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedSignerUpdateMessage.signerUpdate.common.nonce) - .to.emit(accountVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedSignerUpdateMessage.action.common.nonce) - - // confirm userB is now a delegated signer - expect(await marketFactory.signers(userA.address, userB.address)).to.be.true - }) - - it('relays batch access update messages', async () => { - // confirm userB is not already an operator, and userC is not already a delegated signer - expect(await marketFactory.operators(userA.address, userB.address)).to.be.false - expect(await marketFactory.signers(userA.address, userC.address)).to.be.false - - // create and sign the inner message - const accessUpdateBatch = { - operators: [{ accessor: userB.address, approved: true }], - signers: [{ accessor: userC.address, approved: true }], - ...createCommon(marketFactory.address), - } - const innerSignature = await signAccessUpdateBatch(userA, downstreamVerifier, accessUpdateBatch) - - // create and sign the outer message - const relayedAccessUpdateBatchMesage = { - accessUpdateBatch: accessUpdateBatch, - ...createAction(userA.address), - } - const outerSignature = await signRelayedAccessUpdateBatch(userA, accountVerifier, relayedAccessUpdateBatchMesage) - - // perform the action - await expect( - controller - .connect(keeper) - .relayAccessUpdateBatch(relayedAccessUpdateBatchMesage, outerSignature, innerSignature, TX_OVERRIDES), - ) - .to.emit(marketFactory, 'OperatorUpdated') - .withArgs(userA.address, userB.address, true) - .to.emit(marketFactory, 'SignerUpdated') - .withArgs(userA.address, userC.address, true) - .to.emit(downstreamVerifier, 'NonceCancelled') - .withArgs(userA.address, accessUpdateBatch.common.nonce) - .to.emit(accountVerifier, 'NonceCancelled') - .withArgs(userA.address, relayedAccessUpdateBatchMesage.action.common.nonce) - - // confirm userB is now an operator, and userC a delegated signer - expect(await marketFactory.operators(userA.address, userB.address)).to.be.true - expect(await marketFactory.signers(userA.address, userC.address)).to.be.true - }) - }) -}) +RunCollateralAccountTests('Controller_Arbitrum', deployProtocol, deployInstance, mockGasInfo) diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts new file mode 100644 index 000000000..77b790daf --- /dev/null +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -0,0 +1,965 @@ +import { expect } from 'chai' +import HRE from 'hardhat' +import { Address } from 'hardhat-deploy/dist/types' +import { BigNumber, CallOverrides, constants, utils } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { advanceBlock, currentBlockTimestamp } from '../../../common/testutil/time' +import { getEventArguments } from '../../../common/testutil/transaction' + +import { parse6decimal } from '../../../common/testutil/types' +import { + Account, + Account__factory, + AccountVerifier__factory, + Controller_Incentivized, + IAccount, + IAccountVerifier, + IERC20Metadata, + IMarket, + IMarketFactory, + IVerifier, +} from '../../types/generated' + +import { + signDeployAccount, + signMarketTransfer, + signRebalanceConfigChange, + signRelayedAccessUpdateBatch, + signRelayedGroupCancellation, + signRelayedNonceCancellation, + signRelayedOperatorUpdate, + signRelayedSignerUpdate, + signWithdrawal, +} from '../helpers/erc712' +import { advanceToPrice } from '../helpers/setupHelpers' +import { + signAccessUpdateBatch, + signGroupCancellation, + signCommon as signNonceCancellation, + signOperatorUpdate, + signSignerUpdate, +} from '@equilibria/perennial-v2-verifier/test/helpers/erc712' +import { Verifier, Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' +import { IVerifier__factory } from '@equilibria/perennial-v2/types/generated' +import { IKeeperOracle, IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' + +const { ethers } = HRE + +const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' // price feed used for keeper compensation +const DEFAULT_MAX_FEE = parse6decimal('0.5') + +// hack around issues estimating gas for instrumented contracts when running tests under coverage +const TX_OVERRIDES = { gasLimit: 3_000_000, maxPriorityFeePerGas: 0, maxFeePerGas: 100_000_000 } + +export interface DeploymentVars { + dsu: IERC20Metadata + usdc: IERC20Metadata + oracleFactory: IOracleFactory + pythOracleFactory: PythFactory + marketFactory: IMarketFactory + ethMarket: IMarket + btcMarket: IMarket + ethKeeperOracle: IKeeperOracle + btcKeeperOracle: IKeeperOracle + fundWalletDSU(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise + fundWalletUSDC(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise +} + +export function RunCollateralAccountTests( + name: string, + deployProtocol: (owner: SignerWithAddress, overrides?: CallOverrides) => Promise, + deployInstance: ( + owner: SignerWithAddress, + marketFactory: IMarketFactory, + relayVerifier: IVerifier, + overrides?: CallOverrides, + ) => Promise, + mockGasInfo: () => Promise, +): void { + describe(name, () => { + let deployment: DeploymentVars + let dsu: IERC20Metadata + let usdc: IERC20Metadata + let controller: Controller_Incentivized + let accountVerifier: IAccountVerifier + let marketFactory: IMarketFactory + let ethMarket: IMarket + let btcMarket: IMarket + let ethKeeperOracle: IKeeperOracle + let btcKeeperOracle: IKeeperOracle + let owner: SignerWithAddress + let userA: SignerWithAddress + let userB: SignerWithAddress + let userC: SignerWithAddress + let keeper: SignerWithAddress + let receiver: SignerWithAddress + let lastNonce = 0 + let currentTime: BigNumber + let keeperBalanceBefore: BigNumber + let keeperEthBalanceBefore: BigNumber + + // create a default action for the specified user with reasonable fee and expiry + function createAction( + userAddress: Address, + signerAddress = userAddress, + maxFee = DEFAULT_MAX_FEE, + expiresInSeconds = 45, + ) { + return { + action: { + maxFee: maxFee, + common: { + account: userAddress, + signer: signerAddress, + domain: controller.address, + nonce: nextNonce(), + group: 0, + expiry: currentTime.add(expiresInSeconds), + }, + }, + } + } + + // deploys and funds a collateral account + async function createCollateralAccount(user: SignerWithAddress, amount: BigNumber): Promise { + const accountAddress = await controller.getAccountAddress(user.address) + await usdc.connect(userA).transfer(accountAddress, amount, TX_OVERRIDES) + const deployAccountMessage = { + ...createAction(user.address, user.address), + } + const signatureCreate = await signDeployAccount(user, accountVerifier, deployAccountMessage) + const tx = await controller + .connect(keeper) + .deployAccountWithSignature(deployAccountMessage, signatureCreate, TX_OVERRIDES) + + // verify the address from event arguments + const creationArgs = await getEventArguments(tx, 'AccountDeployed') + expect(creationArgs.account).to.equal(accountAddress) + + // approve the collateral account as operator + await marketFactory.connect(user).updateOperator(accountAddress, true, TX_OVERRIDES) + + return Account__factory.connect(accountAddress, user) + } + + async function checkCompensation(priceCommitments = 0) { + const keeperFeesPaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) + let keeperEthSpentOnGas = keeperEthBalanceBefore.sub(await keeper.getBalance()) + + // if TXes in test required outside price commitments, compensate the keeper for them + keeperEthSpentOnGas = keeperEthSpentOnGas.add(utils.parseEther('0.0000644306').mul(priceCommitments)) + + // cost of transaction + const keeperGasCostInUSD = keeperEthSpentOnGas.mul(3413) + // keeper should be compensated between 100-125% of actual gas cost + expect(keeperFeesPaid).to.be.within(keeperGasCostInUSD, keeperGasCostInUSD.mul(125).div(100)) + } + + // create a serial nonce for testing purposes; real users may choose a nonce however they please + function nextNonce(): BigNumber { + lastNonce += 1 + return BigNumber.from(lastNonce) + } + + // deposit from the collateral account to the ETH market + async function deposit(amount: BigNumber, account: IAccount) { + // sign the message + const marketTransferMessage = { + market: ethMarket.address, + amount: amount, + ...createAction(userA.address, userA.address), + } + const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) + + // perform transfer + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ) + .to.emit(dsu, 'Transfer') + .withArgs(account.address, ethMarket.address, anyValue) // scale to token precision + .to.emit(ethMarket, 'OrderCreated') + .withArgs( + userA.address, + anyValue, + anyValue, + constants.AddressZero, + constants.AddressZero, + constants.AddressZero, + ) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + } + + const fixture = async () => { + // deploy the protocol + ;[owner, userA, userB, userC, keeper, receiver] = await ethers.getSigners() + deployment = await deployProtocol(owner, TX_OVERRIDES) + // TODO: consider replacing these 8 member variables with an instance of DeploymentVars + dsu = deployment.dsu + usdc = deployment.usdc + marketFactory = deployment.marketFactory + ethMarket = deployment.ethMarket + btcMarket = deployment.btcMarket + ethKeeperOracle = deployment.ethKeeperOracle + btcKeeperOracle = deployment.btcKeeperOracle + + await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES) + await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('57575.464'), TX_OVERRIDES) + + await dsu.connect(userA).approve(ethMarket.address, constants.MaxUint256, { maxFeePerGas: 100000000 }) + + // set up users and deploy artifacts + const keepConfig = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 275_000, // buffer for handling the keeper fee + multiplierCalldata: ethers.utils.parseEther('1'), + bufferCalldata: 0, + } + const keepConfigBuffered = { + multiplierBase: ethers.utils.parseEther('1.08'), + bufferBase: 1_500_000, // for price commitment + multiplierCalldata: ethers.utils.parseEther('1.08'), + bufferCalldata: 35_200, + } + const keepConfigWithdrawal = { + multiplierBase: ethers.utils.parseEther('1.05'), + bufferBase: 1_500_000, + multiplierCalldata: ethers.utils.parseEther('1.05'), + bufferCalldata: 35_200, + } + const marketVerifier = IVerifier__factory.connect(await marketFactory.verifier(), owner) + controller = await deployInstance(owner, marketFactory, marketVerifier, { + maxFeePerGas: 100000000, + }) + accountVerifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address, { + maxFeePerGas: 100000000, + }) + // chainlink feed is used by Kept for keeper compensation + const KeepConfig = '(uint256,uint256,uint256,uint256)' + await controller[`initialize(address,address,${KeepConfig},${KeepConfig},${KeepConfig})`]( + accountVerifier.address, + CHAINLINK_ETH_USD_FEED, + keepConfig, + keepConfigBuffered, + keepConfigWithdrawal, + ) + // fund userA + await deployment.fundWalletUSDC(userA, parse6decimal('50000'), { maxFeePerGas: 100000000 }) + } + + before(async () => { + // touch the provider, such that smock doesn't error out running a single test + await advanceBlock() + // mock gas information for the chain being tested + await mockGasInfo() + }) + + beforeEach(async () => { + // update the timestamp used for calculating expiry and adjusting oracle price + currentTime = BigNumber.from(await currentBlockTimestamp()) + await loadFixture(fixture) + + // set a realistic base gas fee + await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x5F5E100']) // 0.1 gwei + + keeperBalanceBefore = await dsu.balanceOf(keeper.address) + keeperEthBalanceBefore = await keeper.getBalance() + currentTime = BigNumber.from(await currentBlockTimestamp()) + }) + + afterEach(async () => { + // ensure controller has no funds at rest + expect(await dsu.balanceOf(controller.address)).to.equal(0) + + // reset to avoid impact to setup and other tests + await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x1']) + }) + + describe('#deployment', () => { + let accountAddressA: Address + + // fund the account with 15k USDC + beforeEach(async () => { + accountAddressA = await controller.getAccountAddress(userA.address) + }) + + it('can create an account', async () => { + // pre-fund the address where the account will be deployed + await usdc.connect(userA).transfer(accountAddressA, parse6decimal('15000'), TX_OVERRIDES) + + // sign a message to deploy the account + const deployAccountMessage = { + ...createAction(userA.address, userA.address), + } + const signature = await signDeployAccount(userA, accountVerifier, deployAccountMessage) + + // keeper executes deployment of the account and is compensated + await expect( + controller.connect(keeper).deployAccountWithSignature(deployAccountMessage, signature, TX_OVERRIDES), + ) + .to.emit(controller, 'AccountDeployed') + .withArgs(userA.address, accountAddressA) + + await checkCompensation() + }) + + it('keeper fee is limited by maxFee', async () => { + // pre-fund the address where the account will be deployed + await usdc.connect(userA).transfer(accountAddressA, parse6decimal('15000'), TX_OVERRIDES) + + // sign a message with maxFee smaller than the calculated keeper fee (~0.0033215) + const maxFee = parse6decimal('0.0789') + const deployAccountMessage = { + ...createAction(userA.address, userA.address, maxFee), + } + const signature = await signDeployAccount(userA, accountVerifier, deployAccountMessage) + + // keeper executes deployment of the account and is compensated + await expect( + controller.connect(keeper).deployAccountWithSignature(deployAccountMessage, signature, TX_OVERRIDES), + ) + .to.emit(controller, 'AccountDeployed') + .withArgs(userA.address, accountAddressA) + + const keeperFeePaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) + expect(keeperFeePaid).to.equal(maxFee.mul(1e12)) // convert from 6- to 18- decimal + }) + + it('reverts if keeper cannot be compensated', async () => { + // ensure the account is empty + expect(await dsu.balanceOf(keeper.address)).to.equal(0) + expect(await usdc.balanceOf(keeper.address)).to.equal(0) + + // sign a message to deploy the account + const deployAccountMessage = { + ...createAction(userA.address, userA.address), + } + + // ensure the request fails + const signature = await signDeployAccount(userA, accountVerifier, deployAccountMessage) + await expect( + controller + .connect(keeper) + .deployAccountWithSignature(deployAccountMessage, signature, { maxFeePerGas: 100000000 }), + ).to.be.reverted + }) + }) + + describe('#transfer', async () => { + const INITIAL_DEPOSIT_6 = parse6decimal('13000') + let accountA: Account + + beforeEach(async () => { + // deploy collateral account for userA + accountA = await createCollateralAccount(userA, INITIAL_DEPOSIT_6) + }) + + it('collects fee for depositing some funds to market', async () => { + // sign a message to deposit 6k from the collateral account to the market + const transferAmount = parse6decimal('6000') + const marketTransferMessage = { + market: ethMarket.address, + amount: transferAmount, + ...createAction(userA.address), + } + const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) + + // perform transfer + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, ethMarket.address, transferAmount.mul(1e12)) // scale to token precision + .to.emit(ethMarket, 'OrderCreated') + .withArgs( + userA.address, + anyValue, + anyValue, + constants.AddressZero, + constants.AddressZero, + constants.AddressZero, + ) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(transferAmount) + + await checkCompensation(1) + }) + + it('collects fee for withdrawing some funds from market', async () => { + // user deposits collateral to the market + await deposit(parse6decimal('12000'), accountA) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(parse6decimal('12000')) + + // sign a message to make a partial withdrawal + const withdrawal = parse6decimal('-2000') + const marketTransferMessage = { + market: ethMarket.address, + amount: withdrawal, + ...createAction(userA.address, userA.address), + } + const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) + + // perform transfer + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ) + .to.emit(dsu, 'Transfer') + .withArgs(ethMarket.address, accountA.address, withdrawal.mul(-1e12)) // scale to token precision + .to.emit(ethMarket, 'OrderCreated') + .withArgs( + userA.address, + anyValue, + anyValue, + constants.AddressZero, + constants.AddressZero, + constants.AddressZero, + ) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(parse6decimal('10000')) // 12k-2k + + await checkCompensation(2) + }) + + it('collects fee for withdrawing native deposit from market', async () => { + // user directly deposits collateral to the market + const depositAmount = parse6decimal('13000') + await deployment.fundWalletDSU(userA, depositAmount.mul(1e12), TX_OVERRIDES) + await ethMarket + .connect(userA) + ['update(address,uint256,uint256,uint256,int256,bool)']( + userA.address, + constants.MaxUint256, + constants.MaxUint256, + constants.MaxUint256, + depositAmount, + false, + { maxFeePerGas: 150000000 }, + ) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(depositAmount) + + // sign a message to withdraw everything from the market back into the collateral account + const marketTransferMessage = { + market: ethMarket.address, + amount: constants.MinInt256, + ...createAction(userA.address, userA.address), + } + const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) + + // perform transfer + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ) + .to.emit(dsu, 'Transfer') + .withArgs(ethMarket.address, accountA.address, depositAmount.mul(1e12)) // scale to token precision + .to.emit(ethMarket, 'OrderCreated') + .withArgs( + userA.address, + anyValue, + anyValue, + constants.AddressZero, + constants.AddressZero, + constants.AddressZero, + ) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(0) + + await checkCompensation(1) + }) + + it('collects fee for withdrawing funds into empty collateral account', async () => { + // deposit 12k + await deposit(parse6decimal('12000'), accountA) + // withdraw dust so it cannot be used to pay the keeper + await accountA.withdraw(constants.MaxUint256, true, TX_OVERRIDES) + expect(await dsu.balanceOf(accountA.address)).to.equal(0) + + // sign a message to withdraw 2k from the market back into the collateral account + const withdrawal = parse6decimal('-2000') + const marketTransferMessage = { + market: ethMarket.address, + amount: withdrawal, + ...createAction(userA.address, userA.address), + } + const signature = await signMarketTransfer(userA, accountVerifier, marketTransferMessage) + + // perform transfer + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ) + .to.emit(dsu, 'Transfer') + .withArgs(ethMarket.address, accountA.address, anyValue) + .to.emit(ethMarket, 'OrderCreated') + .withArgs( + userA.address, + anyValue, + anyValue, + constants.AddressZero, + constants.AddressZero, + constants.AddressZero, + ) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + expect((await ethMarket.locals(userA.address)).collateral).to.be.within( + parse6decimal('9999'), + parse6decimal('10000'), + ) // 12k-2k + + await checkCompensation(2) + }) + }) + + describe('#rebalance', async () => { + let accountA: Account + + beforeEach(async () => { + accountA = await createCollateralAccount(userA, parse6decimal('10005')) + }) + + it('collects fee for changing rebalance configuration', async () => { + // sign message to create a new group + const message = { + group: 5, + markets: [ethMarket.address], + configs: [{ target: parse6decimal('1'), threshold: parse6decimal('0.0901') }], + maxFee: DEFAULT_MAX_FEE, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, accountVerifier, message) + + // create the group + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)) + .to.emit(controller, 'RebalanceMarketConfigured') + .withArgs(userA.address, message.group, ethMarket.address, message.configs[0]) + .to.emit(controller, 'RebalanceGroupConfigured') + .withArgs(userA.address, message.group, 1) + + // ensure keeper was compensated + await checkCompensation() + }) + + it('collects fee for rebalancing a group', async () => { + // create a new group with two markets + const message = { + group: 4, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, + { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, + ], + maxFee: DEFAULT_MAX_FEE, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, accountVerifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)).to + .not.be.reverted + + // transfer all collateral to ethMarket + await deposit(parse6decimal('10000'), accountA) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(parse6decimal('10000')) + expect((await btcMarket.locals(userA.address)).collateral).to.equal(0) + + // rebalance the group + await expect(controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES)) + .to.emit(dsu, 'Transfer') + .withArgs(ethMarket.address, accountA.address, utils.parseEther('5000')) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, btcMarket.address, utils.parseEther('5000')) + .to.emit(controller, 'GroupRebalanced') + .withArgs(userA.address, 4) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + + // confirm keeper earned their fee + await checkCompensation(2) + }) + + it('honors max rebalance fee when rebalancing a group', async () => { + // create a new group with two markets and a maxFee smaller than the actual fee + const message = { + group: 4, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('0.75'), threshold: parse6decimal('0.06') }, + { target: parse6decimal('0.25'), threshold: parse6decimal('0.06') }, + ], + maxFee: parse6decimal('0.00923'), + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, accountVerifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)).to + .not.be.reverted + + // transfer some collateral to ethMarket and record keeper balance after account creation + await deposit(parse6decimal('5000'), accountA) + const keeperBalanceBefore = await dsu.balanceOf(keeper.address) + + // rebalance the group + await expect(controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES)) + .to.emit(dsu, 'Transfer') + .withArgs(ethMarket.address, accountA.address, utils.parseEther('1250')) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, btcMarket.address, utils.parseEther('1250')) + .to.emit(controller, 'GroupRebalanced') + .withArgs(userA.address, 4) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + + // confirm keeper fee was limited as configured + const keeperFeePaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) + expect(keeperFeePaid).to.equal(utils.parseEther('0.00923')) + }) + + it('cannot award more keeper fees than collateral rebalanced', async () => { + // create a new group with two markets + const message = { + group: 4, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, + { target: parse6decimal('0.5'), threshold: parse6decimal('0.05') }, + ], + maxFee: DEFAULT_MAX_FEE, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, accountVerifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature, TX_OVERRIDES)).to + .not.be.reverted + + let dustAmount = parse6decimal('0.000001') + await dsu.connect(keeper).approve(ethMarket.address, dustAmount.mul(1e12), TX_OVERRIDES) + + // keeper dusts one of the markets + await ethMarket + .connect(keeper) + ['update(address,uint256,uint256,uint256,int256,bool)']( + userA.address, + constants.MaxUint256, + constants.MaxUint256, + constants.MaxUint256, + dustAmount, + false, + { maxFeePerGas: 150000000 }, + ) + expect((await ethMarket.locals(userA.address)).collateral).to.equal(dustAmount) + + // keeper cannot rebalance because dust did not exceed maxFee + await expect( + controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES), + ).to.be.revertedWithCustomError(controller, 'ControllerGroupBalancedError') + + // keeper dusts the other market, such that target is nonzero, and percentage exceeded + dustAmount = parse6decimal('0.000003') + await dsu.connect(keeper).approve(btcMarket.address, dustAmount.mul(1e12), TX_OVERRIDES) + await btcMarket + .connect(keeper) + ['update(address,uint256,uint256,uint256,int256,bool)']( + userA.address, + constants.MaxUint256, + constants.MaxUint256, + constants.MaxUint256, + dustAmount, + false, + { maxFeePerGas: 150000000 }, + ) + expect((await btcMarket.locals(userA.address)).collateral).to.equal(dustAmount) + + // keeper still cannot rebalance because dust did not exceed maxFee + await expect( + controller.connect(keeper).rebalanceGroup(userA.address, 4, TX_OVERRIDES), + ).to.be.revertedWithCustomError(controller, 'ControllerGroupBalancedError') + }) + }) + + describe('#withdrawal', async () => { + let accountA: Account + let userBalanceBefore: BigNumber + + beforeEach(async () => { + // deploy collateral account for userA + accountA = await createCollateralAccount(userA, parse6decimal('17000')) + userBalanceBefore = await usdc.balanceOf(userA.address) + }) + + afterEach(async () => { + // confirm keeper earned their fee + await checkCompensation(1) + }) + + it('collects fee for partial withdrawal from a delegated signer', async () => { + // configure userB as delegated signer + await marketFactory.connect(userA).updateSigner(userB.address, true, TX_OVERRIDES) + + // delegate signs message for partial withdrawal + const withdrawalAmount = parse6decimal('7000') + const withdrawalMessage = { + amount: withdrawalAmount, + unwrap: true, + ...createAction(userA.address, userB.address), + } + const signature = await signWithdrawal(userB, accountVerifier, withdrawalMessage) + + // perform withdrawal and check balance + await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature, TX_OVERRIDES)) + .to.emit(usdc, 'Transfer') + .withArgs(accountA.address, userA.address, anyValue) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + + // confirm userA withdrew their funds and keeper fee was paid from the collateral account + expect(await usdc.balanceOf(accountA.address)).to.be.within(parse6decimal('9999'), parse6decimal('10000')) + expect(await usdc.balanceOf(userA.address)).to.equal(userBalanceBefore.add(withdrawalAmount)) + }) + + it('collects fee for full withdrawal', async () => { + // sign a message to withdraw all funds from the account + const withdrawalMessage = { + amount: constants.MaxUint256, + unwrap: true, + ...createAction(userA.address, userA.address), + } + const signature = await signWithdrawal(userA, accountVerifier, withdrawalMessage) + + // perform withdrawal and check balances + await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature, TX_OVERRIDES)) + .to.emit(usdc, 'Transfer') + .withArgs(accountA.address, userA.address, anyValue) + .to.emit(controller, 'KeeperCall') + .withArgs(keeper.address, anyValue, 0, anyValue, anyValue, anyValue) + + // collateral account should be empty + expect(await dsu.balanceOf(accountA.address)).to.equal(0) + expect(await usdc.balanceOf(accountA.address)).to.equal(0) + + // user should have their initial balance, plus what was in their collateral account, minus keeper fees + expect(await usdc.balanceOf(userA.address)).to.be.within(parse6decimal('49999'), parse6decimal('50000')) + }) + }) + + describe('#relay', async () => { + let downstreamVerifier: Verifier + + function createCommon(domain: Address) { + return { + common: { + account: userA.address, + signer: userA.address, + domain: domain, + nonce: nextNonce(), + group: 0, + expiry: currentTime.add(60), + }, + } + } + + beforeEach(async () => { + await createCollateralAccount(userA, parse6decimal('6')) + downstreamVerifier = Verifier__factory.connect(await marketFactory.verifier(), owner) + downstreamVerifier.initialize(marketFactory.address, TX_OVERRIDES) + }) + + afterEach(async () => { + // confirm keeper earned their fee + await checkCompensation() + }) + + it('relays nonce cancellation messages', async () => { + // confirm nonce was not already cancelled + const nonce = 7 + expect(await downstreamVerifier.nonces(userA.address, nonce)).to.eq(false) + + // create and sign the inner message + const nonceCancellation = { + account: userA.address, + signer: userA.address, + domain: downstreamVerifier.address, + nonce: nonce, + group: 0, + expiry: currentTime.add(60), + } + const innerSignature = await signNonceCancellation(userA, downstreamVerifier, nonceCancellation) + + // create and sign the outer message + const relayedNonceCancellation = { + nonceCancellation: nonceCancellation, + ...createAction(userA.address, userA.address), + } + const outerSignature = await signRelayedNonceCancellation(userA, accountVerifier, relayedNonceCancellation) + + // perform the action + await expect( + controller + .connect(keeper) + .relayNonceCancellation(relayedNonceCancellation, outerSignature, innerSignature, TX_OVERRIDES), + ) + .to.emit(downstreamVerifier, 'NonceCancelled') + .withArgs(userA.address, nonce) + .to.emit(accountVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedNonceCancellation.action.common.nonce) + + // confirm nonce is now cancelled + expect(await downstreamVerifier.nonces(userA.address, nonce)).to.eq(true) + }) + + it('relays group cancellation messages', async () => { + // confirm group was not already cancelled + const group = 7 + expect(await downstreamVerifier.groups(userA.address, group)).to.eq(false) + + // create and sign the inner message + const groupCancellation = { + group: group, + ...createCommon(downstreamVerifier.address), + } + const innerSignature = await signGroupCancellation(userA, downstreamVerifier, groupCancellation) + + // create and sign the outer message + const relayedGroupCancellation = { + groupCancellation: groupCancellation, + ...createAction(userA.address, userA.address), + } + const outerSignature = await signRelayedGroupCancellation(userA, accountVerifier, relayedGroupCancellation) + + // perform the action + await expect( + controller + .connect(keeper) + .relayGroupCancellation(relayedGroupCancellation, outerSignature, innerSignature, TX_OVERRIDES), + ) + .to.emit(downstreamVerifier, 'GroupCancelled') + .withArgs(userA.address, group) + .to.emit(downstreamVerifier, 'NonceCancelled') + .withArgs(userA.address, groupCancellation.common.nonce) + .to.emit(accountVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedGroupCancellation.action.common.nonce) + + // confirm group is now cancelled + expect(await downstreamVerifier.groups(userA.address, group)).to.eq(true) + }) + + it('relays operator update messages', async () => { + // confirm userB is not already an operator + expect(await marketFactory.operators(userA.address, userB.address)).to.be.false + + // create and sign the inner message + const operatorUpdate = { + access: { + accessor: userB.address, + approved: true, + }, + ...createCommon(marketFactory.address), + } + const innerSignature = await signOperatorUpdate(userA, downstreamVerifier, operatorUpdate) + + // create and sign the outer message + const relayedOperatorUpdateMessage = { + operatorUpdate: operatorUpdate, + ...createAction(userA.address, userA.address), + } + const outerSignature = await signRelayedOperatorUpdate(userA, accountVerifier, relayedOperatorUpdateMessage) + + // perform the action + await expect( + controller + .connect(keeper) + .relayOperatorUpdate(relayedOperatorUpdateMessage, outerSignature, innerSignature, TX_OVERRIDES), + ) + .to.emit(marketFactory, 'OperatorUpdated') + .withArgs(userA.address, userB.address, true) + .to.emit(downstreamVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedOperatorUpdateMessage.operatorUpdate.common.nonce) + .to.emit(accountVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedOperatorUpdateMessage.action.common.nonce) + + // confirm userB is now an operator + expect(await marketFactory.operators(userA.address, userB.address)).to.be.true + }) + + it('relays signer update messages', async () => { + // confirm userB is not already a delegated signer + expect(await marketFactory.signers(userA.address, userB.address)).to.be.false + + // create and sign the inner message + const signerUpdate = { + access: { + accessor: userB.address, + approved: true, + }, + ...createCommon(marketFactory.address), + } + const innerSignature = await signSignerUpdate(userA, downstreamVerifier, signerUpdate) + + // create and sign the outer message + const relayedSignerUpdateMessage = { + signerUpdate: signerUpdate, + ...createAction(userA.address, userA.address), + } + const outerSignature = await signRelayedSignerUpdate(userA, accountVerifier, relayedSignerUpdateMessage) + + // perform the action + await expect( + controller + .connect(keeper) + .relaySignerUpdate(relayedSignerUpdateMessage, outerSignature, innerSignature, TX_OVERRIDES), + ) + .to.emit(marketFactory, 'SignerUpdated') + .withArgs(userA.address, userB.address, true) + .to.emit(downstreamVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedSignerUpdateMessage.signerUpdate.common.nonce) + .to.emit(accountVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedSignerUpdateMessage.action.common.nonce) + + // confirm userB is now a delegated signer + expect(await marketFactory.signers(userA.address, userB.address)).to.be.true + }) + + it('relays batch access update messages', async () => { + // confirm userB is not already an operator, and userC is not already a delegated signer + expect(await marketFactory.operators(userA.address, userB.address)).to.be.false + expect(await marketFactory.signers(userA.address, userC.address)).to.be.false + + // create and sign the inner message + const accessUpdateBatch = { + operators: [{ accessor: userB.address, approved: true }], + signers: [{ accessor: userC.address, approved: true }], + ...createCommon(marketFactory.address), + } + const innerSignature = await signAccessUpdateBatch(userA, downstreamVerifier, accessUpdateBatch) + + // create and sign the outer message + const relayedAccessUpdateBatchMesage = { + accessUpdateBatch: accessUpdateBatch, + ...createAction(userA.address), + } + const outerSignature = await signRelayedAccessUpdateBatch( + userA, + accountVerifier, + relayedAccessUpdateBatchMesage, + ) + + // perform the action + await expect( + controller + .connect(keeper) + .relayAccessUpdateBatch(relayedAccessUpdateBatchMesage, outerSignature, innerSignature, TX_OVERRIDES), + ) + .to.emit(marketFactory, 'OperatorUpdated') + .withArgs(userA.address, userB.address, true) + .to.emit(marketFactory, 'SignerUpdated') + .withArgs(userA.address, userC.address, true) + .to.emit(downstreamVerifier, 'NonceCancelled') + .withArgs(userA.address, accessUpdateBatch.common.nonce) + .to.emit(accountVerifier, 'NonceCancelled') + .withArgs(userA.address, relayedAccessUpdateBatchMesage.action.common.nonce) + + // confirm userB is now an operator, and userC a delegated signer + expect(await marketFactory.operators(userA.address, userB.address)).to.be.true + expect(await marketFactory.signers(userA.address, userC.address)).to.be.true + }) + }) + }) +} From 7a8e925797fd71bdf92896787a6a5d99278b773e Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Mon, 14 Oct 2024 15:52:47 -0400 Subject: [PATCH 04/23] refactored integration test setup --- .../test/helpers/arbitrumHelpers.ts | 142 +----------------- .../test/helpers/setupHelpers.ts | 137 +++++++++++++++++ .../test/integration/Account.test.ts | 4 +- .../test/integration/Controller.test.ts | 8 +- .../integration/Controller_Arbitrum.test.ts | 11 +- 5 files changed, 151 insertions(+), 151 deletions(-) diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index 12c05f735..bfda0bf51 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -1,17 +1,8 @@ import { expect } from 'chai' import { BigNumber, CallOverrides, constants, utils } from 'ethers' -import { - IKeeperOracle, - IOracleFactory, - KeeperOracle, - KeeperOracle__factory, - Oracle, - Oracle__factory, - PythFactory, - PythFactory__factory, -} from '@equilibria/perennial-v2-oracle/types/generated' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { createMarket, deployController, deployOracleFactory, deployProtocolForOracle } from './setupHelpers' +import { IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' +import { createFactories, deployController } from './setupHelpers' import { Account__factory, AccountVerifier__factory, @@ -20,17 +11,12 @@ import { Controller_Arbitrum__factory, IERC20Metadata, IERC20Metadata__factory, - IMarket, IMarketFactory, - IOracleProvider, - GasOracle__factory, } from '../../types/generated' import { impersonate } from '../../../common/testutil' import { IVerifier } from '@equilibria/perennial-v2/types/generated' const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' -const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' -const PYTH_BTC_USD_PRICE_FEED = '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43' const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' const DSU_ADDRESS = '0x52C64b8998eB7C80b6F526E99E29ABdcC86B841b' // Digital Standard Unit, an 18-decimal token @@ -41,98 +27,10 @@ const USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' // Arbitrum na const USDC_HOLDER = '0x2df1c51e09aecf9cacb7bc98cb1742757f163df7' // Hyperliquid deposit bridge has 414mm USDC at height 233560862 // deploys protocol -export async function createFactories( +export async function createFactoriesForChain( owner: SignerWithAddress, ): Promise<[IOracleFactory, IMarketFactory, PythFactory]> { - // Deploy the oracle factory, which markets created by the market factory will query - const oracleFactory = await deployOracleFactory(owner) - // Deploy the market factory and authorize it with the oracle factory - const marketFactory = await deployProtocolForOracle(owner, oracleFactory) - - // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices - - const commitmentGasOracle = await new GasOracle__factory(owner).deploy( - CHAINLINK_ETH_USD_FEED, - 8, - 1_000_000, - utils.parseEther('1.02'), - 1_000_000, - 0, - 0, - 0, - ) - const settlementGasOracle = await new GasOracle__factory(owner).deploy( - CHAINLINK_ETH_USD_FEED, - 8, - 200_000, - utils.parseEther('1.02'), - 500_000, - 0, - 0, - 0, - ) - const keeperOracleImpl = await new KeeperOracle__factory(owner).deploy(60) - const pythOracleFactory = await new PythFactory__factory(owner).deploy( - PYTH_ADDRESS, - commitmentGasOracle.address, - settlementGasOracle.address, - keeperOracleImpl.address, - ) - await pythOracleFactory.initialize(oracleFactory.address) - await pythOracleFactory.updateParameter(1, 0, 4, 10) - await oracleFactory.register(pythOracleFactory.address) - - return [oracleFactory, marketFactory, pythOracleFactory] -} - -// creates an ETH market using a locally deployed factory and oracle -export async function createMarketETH( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, - pythOracleFactory: PythFactory, - marketFactory: IMarketFactory, - dsu: IERC20Metadata, - overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { - // Create oracles needed to support the market - const [keeperOracle, oracle] = await createPythOracle( - owner, - oracleFactory, - pythOracleFactory, - PYTH_ETH_USD_PRICE_FEED, - 'ETH-USD', - overrides, - ) - // Create the market in which user or collateral account may interact - const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) - await keeperOracle.register(oracle.address) - await oracle.register(market.address) - return [market, oracle, keeperOracle] -} - -// creates a BTC market using a locally deployed factory and oracle -export async function createMarketBTC( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, - pythOracleFactory: PythFactory, - marketFactory: IMarketFactory, - dsu: IERC20Metadata, - overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { - // Create oracles needed to support the market - const [keeperOracle, oracle] = await createPythOracle( - owner, - oracleFactory, - pythOracleFactory, - PYTH_BTC_USD_PRICE_FEED, - 'BTC-USD', - overrides, - ) - // Create the market in which user or collateral account may interact - const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) - await keeperOracle.register(oracle.address) - await oracle.register(market.address) - return [market, oracle, keeperOracle] + return createFactories(owner, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) } // connects to Arbitrum stablecoins and deploys a controller configured for them @@ -205,35 +103,3 @@ export async function returnDSU(wallet: SignerWithAddress): Promise { const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, wallet) await dsu.transfer(DSU_HOLDER, await dsu.balanceOf(wallet.address)) } - -async function createPythOracle( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, - pythOracleFactory: PythFactory, - pythFeedId: string, - name: string, - overrides?: CallOverrides, -): Promise<[KeeperOracle, Oracle]> { - // Create the keeper oracle, which tests may use to meddle with prices - const keeperOracle = KeeperOracle__factory.connect( - await pythOracleFactory.callStatic.create(pythFeedId, pythFeedId, { - provider: constants.AddressZero, - decimals: 0, - }), - owner, - ) - await pythOracleFactory.create( - pythFeedId, - pythFeedId, - { provider: constants.AddressZero, decimals: 0 }, - overrides ?? {}, - ) - - // Create the oracle, which markets created by the market factory will query - const oracle = Oracle__factory.connect( - await oracleFactory.callStatic.create(pythFeedId, pythOracleFactory.address, name), - owner, - ) - await oracleFactory.create(pythFeedId, pythOracleFactory.address, name, overrides ?? {}) - return [keeperOracle, oracle] -} diff --git a/packages/perennial-account/test/helpers/setupHelpers.ts b/packages/perennial-account/test/helpers/setupHelpers.ts index b0c2bc937..8bd143d5d 100644 --- a/packages/perennial-account/test/helpers/setupHelpers.ts +++ b/packages/perennial-account/test/helpers/setupHelpers.ts @@ -40,10 +40,19 @@ import { Oracle__factory, IOracleFactory, IOracle, + PythFactory, + GasOracle__factory, + KeeperOracle__factory, + PythFactory__factory, + KeeperOracle, + Oracle, } from '@equilibria/perennial-v2-oracle/types/generated' import { OracleVersionStruct } from '../../types/generated/@equilibria/perennial-v2/contracts/interfaces/IOracleProvider' import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' +const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' +const PYTH_BTC_USD_PRICE_FEED = '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43' + // Simulates an oracle update from KeeperOracle. // If timestamp matches a requested version, callbacks implicitly settle the market. export async function advanceToPrice( @@ -76,6 +85,52 @@ export async function advanceToPrice( return await getTimestamp(tx) } +// deploys market and oracle factories +export async function createFactories( + owner: SignerWithAddress, + pythAddress: Address, + chainLinkFeedAddress: Address, +): Promise<[IOracleFactory, IMarketFactory, PythFactory]> { + // Deploy the oracle factory, which markets created by the market factory will query + const oracleFactory = await deployOracleFactory(owner) + // Deploy the market factory and authorize it with the oracle factory + const marketFactory = await deployProtocolForOracle(owner, oracleFactory) + + // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices + const commitmentGasOracle = await new GasOracle__factory(owner).deploy( + chainLinkFeedAddress, + 8, + 1_000_000, + utils.parseEther('1.02'), + 1_000_000, + 0, + 0, + 0, + ) + const settlementGasOracle = await new GasOracle__factory(owner).deploy( + chainLinkFeedAddress, + 8, + 200_000, + utils.parseEther('1.02'), + 500_000, + 0, + 0, + 0, + ) + const keeperOracleImpl = await new KeeperOracle__factory(owner).deploy(60) + const pythOracleFactory = await new PythFactory__factory(owner).deploy( + pythAddress, + commitmentGasOracle.address, + settlementGasOracle.address, + keeperOracleImpl.address, + ) + await pythOracleFactory.initialize(oracleFactory.address) + await pythOracleFactory.updateParameter(1, 0, 4, 10) + await oracleFactory.register(pythOracleFactory.address) + + return [oracleFactory, marketFactory, pythOracleFactory] +} + // Using a provided factory, create a new market and set some reasonable initial parameters export async function createMarket( owner: SignerWithAddress, @@ -149,6 +204,88 @@ export async function createMarket( return market } +// creates an ETH market using a locally deployed factory and oracle +export async function createMarketETH( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythOracleFactory: PythFactory, + marketFactory: IMarketFactory, + dsu: IERC20Metadata, + overrides?: CallOverrides, +): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { + // Create oracles needed to support the market + const [keeperOracle, oracle] = await createPythOracle( + owner, + oracleFactory, + pythOracleFactory, + PYTH_ETH_USD_PRICE_FEED, + 'ETH-USD', + overrides, + ) + // Create the market in which user or collateral account may interact + const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) + await keeperOracle.register(oracle.address) + await oracle.register(market.address) + return [market, oracle, keeperOracle] +} + +// creates a BTC market using a locally deployed factory and oracle +export async function createMarketBTC( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythOracleFactory: PythFactory, + marketFactory: IMarketFactory, + dsu: IERC20Metadata, + overrides?: CallOverrides, +): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { + // Create oracles needed to support the market + const [keeperOracle, oracle] = await createPythOracle( + owner, + oracleFactory, + pythOracleFactory, + PYTH_BTC_USD_PRICE_FEED, + 'BTC-USD', + overrides, + ) + // Create the market in which user or collateral account may interact + const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) + await keeperOracle.register(oracle.address) + await oracle.register(market.address) + return [market, oracle, keeperOracle] +} + +export async function createPythOracle( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythOracleFactory: PythFactory, + pythFeedId: string, + name: string, + overrides?: CallOverrides, +): Promise<[KeeperOracle, Oracle]> { + // Create the keeper oracle, which tests may use to meddle with prices + const keeperOracle = KeeperOracle__factory.connect( + await pythOracleFactory.callStatic.create(pythFeedId, pythFeedId, { + provider: constants.AddressZero, + decimals: 0, + }), + owner, + ) + await pythOracleFactory.create( + pythFeedId, + pythFeedId, + { provider: constants.AddressZero, decimals: 0 }, + overrides ?? {}, + ) + + // Create the oracle, which markets created by the market factory will query + const oracle = Oracle__factory.connect( + await oracleFactory.callStatic.create(pythFeedId, pythOracleFactory.address, name), + owner, + ) + await oracleFactory.create(pythFeedId, pythOracleFactory.address, name, overrides ?? {}) + return [keeperOracle, oracle] +} + export async function deployController( owner: SignerWithAddress, usdcAddress: Address, diff --git a/packages/perennial-account/test/integration/Account.test.ts b/packages/perennial-account/test/integration/Account.test.ts index 351146263..bab2aa8b7 100644 --- a/packages/perennial-account/test/integration/Account.test.ts +++ b/packages/perennial-account/test/integration/Account.test.ts @@ -6,7 +6,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { parse6decimal } from '../../../common/testutil/types' import { Account, Account__factory, IController, IERC20Metadata } from '../../types/generated' import { - createFactories, + createFactoriesForChain, deployAndInitializeController, fundWalletDSU, fundWalletUSDC, @@ -33,7 +33,7 @@ describe('Account', () => { const fixture = async () => { ;[owner, userA, userB] = await ethers.getSigners() - const [, marketFactory] = await createFactories(owner) + const [, marketFactory] = await createFactoriesForChain(owner) ;[dsu, usdc, controller] = await deployAndInitializeController(owner, marketFactory) // fund users with some DSU and USDC diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index ebfe0cb2a..a7325321c 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -19,11 +19,9 @@ import { } from '@equilibria/perennial-v2-oracle/types/generated' import { IMarket, IMarketFactory } from '@equilibria/perennial-v2/types/generated' import { signDeployAccount, signMarketTransfer, signRebalanceConfigChange, signWithdrawal } from '../helpers/erc712' -import { advanceToPrice } from '../helpers/setupHelpers' +import { advanceToPrice, createMarketBTC, createMarketETH } from '../helpers/setupHelpers' import { - createFactories, - createMarketBTC, - createMarketETH, + createFactoriesForChain, deployAndInitializeController, fundWalletDSU, fundWalletUSDC, @@ -174,7 +172,7 @@ describe('ControllerBase', () => { ;[owner, userA, userB, keeper, receiver] = await ethers.getSigners() // deploy controller - ;[oracleFactory, marketFactory, pythOracleFactory] = await createFactories(owner) + ;[oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) ;[dsu, usdc, controller] = await deployAndInitializeController(owner, marketFactory) verifier = IAccountVerifier__factory.connect(await controller.verifier(), owner) diff --git a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts index 9677423f2..51d261cdc 100644 --- a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts @@ -1,17 +1,16 @@ import { smock } from '@defi-wonderland/smock' import { use } from 'chai' -import { CallOverrides, Signer } from 'ethers' +import { CallOverrides } from 'ethers' -import { ArbGasInfo } from '../../../types/generated' +import { ArbGasInfo } from '../../types/generated' import { - createMarketBTC, - createMarketETH, - createFactories, + createFactoriesForChain, deployControllerArbitrum, fundWalletDSU, fundWalletUSDC, getStablecoins, } from '../helpers/arbitrumHelpers' +import { createMarketBTC, createMarketETH } from '../helpers/setupHelpers' import { DeploymentVars, RunCollateralAccountTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/generated' @@ -19,7 +18,7 @@ import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/ use(smock.matchers) async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { - const [oracleFactory, marketFactory, pythOracleFactory] = await createFactories(owner) + const [oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) const [ethMarket, , ethKeeperOracle] = await createMarketETH( owner, From 8de71a569c6b9d7da07ab4072cde48272a82462f Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Tue, 15 Oct 2024 09:42:58 -0400 Subject: [PATCH 05/23] commiting wip to switch to another task --- packages/perennial-account/package.json | 1 + .../test/helpers/baseHelpers.ts | 99 +++++++++++++++++++ .../test/helpers/setupHelpers.ts | 6 ++ .../test/integration/Account.test.ts | 2 +- .../test/integration/Controller.test.ts | 2 +- .../integration/Controller_Arbitrum.test.ts | 6 +- .../Controller_Incentivized.test.ts | 4 +- .../integration/Controller_Optimism.test.ts | 77 +++++++++++++++ 8 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 packages/perennial-account/test/helpers/baseHelpers.ts create mode 100644 packages/perennial-account/test/integration/Controller_Optimism.test.ts diff --git a/packages/perennial-account/package.json b/packages/perennial-account/package.json index 5c8b0bcd6..b4b2a585e 100644 --- a/packages/perennial-account/package.json +++ b/packages/perennial-account/package.json @@ -14,6 +14,7 @@ "gasReport": "REPORT_GAS=true OPTIMIZER_ENABLED=true yarn test:integration", "test": "hardhat test test/unit/*", "test:integration": "FORK_ENABLED=true FORK_NETWORK=arbitrum FORK_BLOCK_NUMBER=233560862 hardhat test test/integration/*", + "test:integrationBase": "FORK_ENABLED=true FORK_NETWORK=base FORK_BLOCK_NUMBER=21067741 NODE_INTERVAL_MINING=250 hardhat test test/integration/*", "coverage": "hardhat coverage --testfiles 'test/unit/*'", "coverage:integration": "FORK_ENABLED=true FORK_NETWORK=arbitrum FORK_BLOCK_NUMBER=233560862 hardhat coverage --testfiles 'test/integration/*'", "lint": "eslint --fix --ext '.ts,.js' ./ && solhint 'contracts/**/*.sol' --fix", diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts new file mode 100644 index 000000000..901ac67a6 --- /dev/null +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -0,0 +1,99 @@ +import { expect } from 'chai' +import { BigNumber, CallOverrides, constants, utils } from 'ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' +import { createFactories, deployController } from './setupHelpers' +import { + Account__factory, + AccountVerifier__factory, + Controller, + Controller_Optimism, + Controller_Optimism__factory, + IEmptySetReserve__factory, + IERC20Metadata, + IERC20Metadata__factory, + IMarketFactory, +} from '../../types/generated' +import { impersonate } from '../../../common/testutil' +import { IVerifier } from '@equilibria/perennial-v2/types/generated' + +const PYTH_ADDRESS = '0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a' +const CHAINLINK_ETH_USD_FEED = '0x4aDC67696bA383F43DD60A9e78F2C97Fbbfc7cb1' // TODO: confirm interface is same + +const DSU_ADDRESS = '0x7b4Adf64B0d60fF97D672E473420203D52562A84' // Digital Standard Unit, an 18-decimal token +const DSU_RESERVE = '0x5FA881826AD000D010977645450292701bc2f56D' + +const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC, a 6-decimal token, used by DSU reserve above +const USDC_HOLDER = '0xF977814e90dA44bFA03b6295A0616a897441aceC' // EOA has 302mm USDC at height 21067741 + +// deploys protocol +export async function createFactoriesForChain( + owner: SignerWithAddress, +): Promise<[IOracleFactory, IMarketFactory, PythFactory]> { + return createFactories(owner, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) +} + +// connects to Base stablecoins and deploys a controller configured for them +export async function deployAndInitializeController( + owner: SignerWithAddress, + marketFactory: IMarketFactory, +): Promise<[IERC20Metadata, IERC20Metadata, Controller]> { + const [dsu, usdc] = await getStablecoins(owner) + const controller = await deployController(owner, usdc.address, dsu.address, DSU_RESERVE, marketFactory.address) + + const verifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address) + await controller.initialize(verifier.address) + return [dsu, usdc, controller] +} + +export async function getStablecoins(owner: SignerWithAddress): Promise<[IERC20Metadata, IERC20Metadata]> { + const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) + return [dsu, usdc] +} + +// deploys an instance of the Controller with keeper compensation mechanisms for OP stack chains +export async function deployControllerOptimism( + owner: SignerWithAddress, + marketFactory: IMarketFactory, + nonceManager: IVerifier, + overrides?: CallOverrides, +): Promise { + const accountImpl = await new Account__factory(owner).deploy(USDC_ADDRESS, DSU_ADDRESS, DSU_RESERVE) + accountImpl.initialize(constants.AddressZero) + const controller = await new Controller_Optimism__factory(owner).deploy( + accountImpl.address, + marketFactory.address, + nonceManager.address, + overrides ?? {}, + ) + return controller +} + +export async function fundWalletDSU( + wallet: SignerWithAddress, + amount: BigNumber, + overrides?: CallOverrides, +): Promise { + const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, wallet) + const reserve = IEmptySetReserve__factory.connect(DSU_RESERVE, wallet) + const balanceBefore = await dsu.balanceOf(wallet.address) + + // fund wallet with USDC and then mint using reserve + await fundWalletUSDC(wallet, amount.div(1e12), overrides) + await reserve.mint(amount) + + expect((await dsu.balanceOf(wallet.address)).sub(balanceBefore)).to.equal(amount) +} + +export async function fundWalletUSDC( + wallet: SignerWithAddress, + amount: BigNumber, + overrides?: CallOverrides, +): Promise { + const usdcOwner = await impersonate.impersonateWithBalance(USDC_HOLDER, utils.parseEther('10')) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, usdcOwner) + + expect(await usdc.balanceOf(USDC_HOLDER)).to.be.greaterThan(amount) + await usdc.transfer(wallet.address, amount, overrides ?? {}) +} diff --git a/packages/perennial-account/test/helpers/setupHelpers.ts b/packages/perennial-account/test/helpers/setupHelpers.ts index 8bd143d5d..685c44f18 100644 --- a/packages/perennial-account/test/helpers/setupHelpers.ts +++ b/packages/perennial-account/test/helpers/setupHelpers.ts @@ -214,6 +214,7 @@ export async function createMarketETH( overrides?: CallOverrides, ): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { // Create oracles needed to support the market + console.log('createPythOracle') const [keeperOracle, oracle] = await createPythOracle( owner, oracleFactory, @@ -223,8 +224,11 @@ export async function createMarketETH( overrides, ) // Create the market in which user or collateral account may interact + console.log('createMarket') const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) + console.log('keeperOracle.register') await keeperOracle.register(oracle.address) + console.log('oracle.register') await oracle.register(market.address) return [market, oracle, keeperOracle] } @@ -270,6 +274,7 @@ export async function createPythOracle( }), owner, ) + console.log('creating pythOracle using pythOracleFactory at', pythOracleFactory.address) await pythOracleFactory.create( pythFeedId, pythFeedId, @@ -278,6 +283,7 @@ export async function createPythOracle( ) // Create the oracle, which markets created by the market factory will query + console.log('connecting to oracle', name) const oracle = Oracle__factory.connect( await oracleFactory.callStatic.create(pythFeedId, pythOracleFactory.address, name), owner, diff --git a/packages/perennial-account/test/integration/Account.test.ts b/packages/perennial-account/test/integration/Account.test.ts index bab2aa8b7..54c5b9ed3 100644 --- a/packages/perennial-account/test/integration/Account.test.ts +++ b/packages/perennial-account/test/integration/Account.test.ts @@ -16,7 +16,7 @@ import { const { ethers } = HRE -describe('Account', () => { +describe.skip('Account', () => { let dsu: IERC20Metadata let usdc: IERC20Metadata let controller: IController diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index a7325321c..8a44c8ddf 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -32,7 +32,7 @@ const { ethers } = HRE // hack around intermittent issues estimating gas const TX_OVERRIDES = { gasLimit: 3_000_000, maxFeePerGas: 200_000_000 } -describe('ControllerBase', () => { +describe.skip('ControllerBase', () => { let dsu: IERC20Metadata let usdc: IERC20Metadata let controller: Controller diff --git a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts index 51d261cdc..cddc9fffa 100644 --- a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts @@ -1,6 +1,7 @@ import { smock } from '@defi-wonderland/smock' import { use } from 'chai' import { CallOverrides } from 'ethers' +import HRE from 'hardhat' import { ArbGasInfo } from '../../types/generated' import { @@ -11,7 +12,7 @@ import { getStablecoins, } from '../helpers/arbitrumHelpers' import { createMarketBTC, createMarketETH } from '../helpers/setupHelpers' -import { DeploymentVars, RunCollateralAccountTests } from './Controller_Incentivized.test' +import { DeploymentVars, RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/generated' @@ -67,4 +68,5 @@ async function mockGasInfo() { gasInfo.getL1BaseFeeEstimate.returns(0) } -RunCollateralAccountTests('Controller_Arbitrum', deployProtocol, deployInstance, mockGasInfo) +if (process.env.FORK_NETWORK === 'arbitrum') + RunIncentivizedTests('Controller_Arbitrum', deployProtocol, deployInstance, mockGasInfo) diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index 77b790daf..c3a2b1de3 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -67,7 +67,7 @@ export interface DeploymentVars { fundWalletUSDC(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise } -export function RunCollateralAccountTests( +export function RunIncentivizedTests( name: string, deployProtocol: (owner: SignerWithAddress, overrides?: CallOverrides) => Promise, deployInstance: ( @@ -195,6 +195,7 @@ export function RunCollateralAccountTests( const fixture = async () => { // deploy the protocol ;[owner, userA, userB, userC, keeper, receiver] = await ethers.getSigners() + console.log('deployProtocol') deployment = await deployProtocol(owner, TX_OVERRIDES) // TODO: consider replacing these 8 member variables with an instance of DeploymentVars dsu = deployment.dsu @@ -205,6 +206,7 @@ export function RunCollateralAccountTests( ethKeeperOracle = deployment.ethKeeperOracle btcKeeperOracle = deployment.btcKeeperOracle + console.log('set initial prices') await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES) await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('57575.464'), TX_OVERRIDES) diff --git a/packages/perennial-account/test/integration/Controller_Optimism.test.ts b/packages/perennial-account/test/integration/Controller_Optimism.test.ts new file mode 100644 index 000000000..88a20fc1b --- /dev/null +++ b/packages/perennial-account/test/integration/Controller_Optimism.test.ts @@ -0,0 +1,77 @@ +import { smock } from '@defi-wonderland/smock' +import { use } from 'chai' +import { CallOverrides } from 'ethers' +import HRE from 'hardhat' + +import { ArbGasInfo, OptGasInfo } from '../../types/generated' +import { + createFactoriesForChain, + deployControllerOptimism, + fundWalletDSU, + fundWalletUSDC, + getStablecoins, +} from '../helpers/baseHelpers' +import { createMarketBTC, createMarketETH } from '../helpers/setupHelpers' +import { DeploymentVars, RunIncentivizedTests } from './Controller_Incentivized.test' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/generated' + +async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { + console.log('createFactoriesForChain') + const [oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) + console.log('getStablecoins') + const [dsu, usdc] = await getStablecoins(owner) + console.log('createMarketETH') + const [ethMarket, , ethKeeperOracle] = await createMarketETH( + owner, + oracleFactory, + pythOracleFactory, + marketFactory, + dsu, + ) + console.log('createMarketBTC') + const [btcMarket, , btcKeeperOracle] = await createMarketBTC( + owner, + oracleFactory, + pythOracleFactory, + marketFactory, + dsu, + overrides, + ) + return { + dsu, + usdc, + oracleFactory, + pythOracleFactory, + marketFactory, + ethMarket, + btcMarket, + ethKeeperOracle, + btcKeeperOracle, + fundWalletDSU, + fundWalletUSDC, + } +} + +async function deployInstance( + owner: SignerWithAddress, + marketFactory: IMarketFactory, + relayVerifier: IVerifier, + overrides?: CallOverrides, +): Promise { + return deployControllerOptimism(owner, marketFactory, relayVerifier, overrides) +} + +async function mockGasInfo() { + const gasInfo = await smock.fake('OptGasInfo', { + address: '0x420000000000000000000000000000000000000F', + }) + gasInfo.getL1GasUsed.returns(0) + gasInfo.getL1GasUsed.returns(0) + gasInfo.l1BaseFee.returns(0) + gasInfo.baseFeeScalar.returns(684000) + gasInfo.decimals.returns(6) +} + +if (process.env.FORK_NETWORK === 'base') + RunIncentivizedTests('Controller_Optimism', deployProtocol, deployInstance, mockGasInfo) From 7b5b183bb08104da9fa022211312631724e7c6cc Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Wed, 16 Oct 2024 10:19:10 -0400 Subject: [PATCH 06/23] refactored account tests and chain-specific runners --- packages/perennial-account/package.json | 2 +- .../test/helpers/arbitrumHelpers.ts | 14 +- .../test/helpers/baseHelpers.ts | 4 +- .../test/helpers/setupHelpers.ts | 25 +- .../test/integration/Account.test.ts | 352 +++++++++--------- ...ller_Arbitrum.test.ts => Arbitrum.test.ts} | 17 +- .../test/integration/Controller.test.ts | 12 +- .../Controller_Incentivized.test.ts | 41 +- ...ller_Optimism.test.ts => Optimism.test.ts} | 22 +- .../test/helpers/arbitrumHelpers.ts | 32 +- 10 files changed, 241 insertions(+), 280 deletions(-) rename packages/perennial-account/test/integration/{Controller_Arbitrum.test.ts => Arbitrum.test.ts} (72%) rename packages/perennial-account/test/integration/{Controller_Optimism.test.ts => Optimism.test.ts} (71%) diff --git a/packages/perennial-account/package.json b/packages/perennial-account/package.json index b4b2a585e..a8647ece7 100644 --- a/packages/perennial-account/package.json +++ b/packages/perennial-account/package.json @@ -14,7 +14,7 @@ "gasReport": "REPORT_GAS=true OPTIMIZER_ENABLED=true yarn test:integration", "test": "hardhat test test/unit/*", "test:integration": "FORK_ENABLED=true FORK_NETWORK=arbitrum FORK_BLOCK_NUMBER=233560862 hardhat test test/integration/*", - "test:integrationBase": "FORK_ENABLED=true FORK_NETWORK=base FORK_BLOCK_NUMBER=21067741 NODE_INTERVAL_MINING=250 hardhat test test/integration/*", + "test:integrationBase": "FORK_ENABLED=true FORK_NETWORK=base FORK_BLOCK_NUMBER=21067741 hardhat test test/integration/*", "coverage": "hardhat coverage --testfiles 'test/unit/*'", "coverage:integration": "FORK_ENABLED=true FORK_NETWORK=arbitrum FORK_BLOCK_NUMBER=233560862 hardhat coverage --testfiles 'test/integration/*'", "lint": "eslint --fix --ext '.ts,.js' ./ && solhint 'contracts/**/*.sol' --fix", diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index bfda0bf51..b05d22d02 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -14,7 +14,6 @@ import { IMarketFactory, } from '../../types/generated' import { impersonate } from '../../../common/testutil' -import { IVerifier } from '@equilibria/perennial-v2/types/generated' const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' @@ -50,7 +49,6 @@ export async function deployAndInitializeController( export async function deployControllerArbitrum( owner: SignerWithAddress, marketFactory: IMarketFactory, - nonceManager: IVerifier, overrides?: CallOverrides, ): Promise { const accountImpl = await new Account__factory(owner).deploy(USDC_ADDRESS, DSU_ADDRESS, DSU_RESERVE) @@ -58,7 +56,7 @@ export async function deployControllerArbitrum( const controller = await new Controller_Arbitrum__factory(owner).deploy( accountImpl.address, marketFactory.address, - nonceManager.address, + await marketFactory.verifier(), overrides ?? {}, ) return controller @@ -93,13 +91,3 @@ export async function getStablecoins(owner: SignerWithAddress): Promise<[IERC20M const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) return [dsu, usdc] } - -export async function returnUSDC(wallet: SignerWithAddress): Promise { - const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, wallet) - await usdc.transfer(USDC_HOLDER, await usdc.balanceOf(wallet.address)) -} - -export async function returnDSU(wallet: SignerWithAddress): Promise { - const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, wallet) - await dsu.transfer(DSU_HOLDER, await dsu.balanceOf(wallet.address)) -} diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts index 901ac67a6..3639ff586 100644 --- a/packages/perennial-account/test/helpers/baseHelpers.ts +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -15,7 +15,6 @@ import { IMarketFactory, } from '../../types/generated' import { impersonate } from '../../../common/testutil' -import { IVerifier } from '@equilibria/perennial-v2/types/generated' const PYTH_ADDRESS = '0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a' const CHAINLINK_ETH_USD_FEED = '0x4aDC67696bA383F43DD60A9e78F2C97Fbbfc7cb1' // TODO: confirm interface is same @@ -56,7 +55,6 @@ export async function getStablecoins(owner: SignerWithAddress): Promise<[IERC20M export async function deployControllerOptimism( owner: SignerWithAddress, marketFactory: IMarketFactory, - nonceManager: IVerifier, overrides?: CallOverrides, ): Promise { const accountImpl = await new Account__factory(owner).deploy(USDC_ADDRESS, DSU_ADDRESS, DSU_RESERVE) @@ -64,7 +62,7 @@ export async function deployControllerOptimism( const controller = await new Controller_Optimism__factory(owner).deploy( accountImpl.address, marketFactory.address, - nonceManager.address, + await marketFactory.verifier(), overrides ?? {}, ) return controller diff --git a/packages/perennial-account/test/helpers/setupHelpers.ts b/packages/perennial-account/test/helpers/setupHelpers.ts index 685c44f18..bcb9c1b8c 100644 --- a/packages/perennial-account/test/helpers/setupHelpers.ts +++ b/packages/perennial-account/test/helpers/setupHelpers.ts @@ -49,10 +49,26 @@ import { } from '@equilibria/perennial-v2-oracle/types/generated' import { OracleVersionStruct } from '../../types/generated/@equilibria/perennial-v2/contracts/interfaces/IOracleProvider' import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' +import { pyth } from '@equilibria/perennial-v2-oracle/types/generated/contracts' +import { expect } from 'chai' const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' const PYTH_BTC_USD_PRICE_FEED = '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43' +export interface DeploymentVars { + dsu: IERC20Metadata + usdc: IERC20Metadata + oracleFactory: IOracleFactory + pythOracleFactory: PythFactory + marketFactory: IMarketFactory + ethMarket: IMarket + btcMarket: IMarket + ethKeeperOracle: IKeeperOracle + btcKeeperOracle: IKeeperOracle + fundWalletDSU(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise + fundWalletUSDC(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise +} + // Simulates an oracle update from KeeperOracle. // If timestamp matches a requested version, callbacks implicitly settle the market. export async function advanceToPrice( @@ -124,7 +140,8 @@ export async function createFactories( settlementGasOracle.address, keeperOracleImpl.address, ) - await pythOracleFactory.initialize(oracleFactory.address) + await pythOracleFactory.connect(owner).initialize(oracleFactory.address) + expect(await pythOracleFactory.owner()).to.equal(owner.address) await pythOracleFactory.updateParameter(1, 0, 4, 10) await oracleFactory.register(pythOracleFactory.address) @@ -214,7 +231,6 @@ export async function createMarketETH( overrides?: CallOverrides, ): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { // Create oracles needed to support the market - console.log('createPythOracle') const [keeperOracle, oracle] = await createPythOracle( owner, oracleFactory, @@ -224,11 +240,8 @@ export async function createMarketETH( overrides, ) // Create the market in which user or collateral account may interact - console.log('createMarket') const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) - console.log('keeperOracle.register') await keeperOracle.register(oracle.address) - console.log('oracle.register') await oracle.register(market.address) return [market, oracle, keeperOracle] } @@ -274,7 +287,6 @@ export async function createPythOracle( }), owner, ) - console.log('creating pythOracle using pythOracleFactory at', pythOracleFactory.address) await pythOracleFactory.create( pythFeedId, pythFeedId, @@ -283,7 +295,6 @@ export async function createPythOracle( ) // Create the oracle, which markets created by the market factory will query - console.log('connecting to oracle', name) const oracle = Oracle__factory.connect( await oracleFactory.callStatic.create(pythFeedId, pythOracleFactory.address, name), owner, diff --git a/packages/perennial-account/test/integration/Account.test.ts b/packages/perennial-account/test/integration/Account.test.ts index 54c5b9ed3..d82e5afe6 100644 --- a/packages/perennial-account/test/integration/Account.test.ts +++ b/packages/perennial-account/test/integration/Account.test.ts @@ -1,186 +1,192 @@ import { expect } from 'chai' import HRE from 'hardhat' -import { constants, utils } from 'ethers' +import { CallOverrides, constants, utils } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { parse6decimal } from '../../../common/testutil/types' -import { Account, Account__factory, IController, IERC20Metadata } from '../../types/generated' import { - createFactoriesForChain, - deployAndInitializeController, - fundWalletDSU, - fundWalletUSDC, - returnDSU, - returnUSDC, -} from '../helpers/arbitrumHelpers' + Account, + Account__factory, + Controller_Incentivized, + IController, + IERC20Metadata, + IMarketFactory, +} from '../../types/generated' +import { DeploymentVars } from '../helpers/setupHelpers' const { ethers } = HRE -describe.skip('Account', () => { - let dsu: IERC20Metadata - let usdc: IERC20Metadata - let controller: IController - let account: Account - let owner: SignerWithAddress - let userA: SignerWithAddress - let userB: SignerWithAddress - - // funds specified wallet with 50k DSU and 100k USDC - async function fundWallet(wallet: SignerWithAddress): Promise { - await fundWalletDSU(wallet, utils.parseEther('50000')) - await fundWalletUSDC(wallet, parse6decimal('100000')) - } - - const fixture = async () => { - ;[owner, userA, userB] = await ethers.getSigners() - const [, marketFactory] = await createFactoriesForChain(owner) - ;[dsu, usdc, controller] = await deployAndInitializeController(owner, marketFactory) - - // fund users with some DSU and USDC - await fundWallet(userA) - await fundWallet(userB) - - // create an empty account - const accountAddress = await controller.connect(userA).callStatic.deployAccount() - await controller.connect(userA).deployAccount() - account = Account__factory.connect(accountAddress, userA) - } - - beforeEach(async () => { - await loadFixture(fixture) - }) - - after(async () => { - // return user funds to avoid impacting other tests - await returnUSDC(userA) - await returnDSU(userA) - }) - - describe('#deposit and withdrawal', () => { - it('can use deposit function to pull USDC into account', async () => { - // run token approval - const depositAmount = parse6decimal('6000') - await usdc.connect(userA).approve(account.address, depositAmount) - - // call the deposit function to transferFrom userA - await expect(account.deposit(depositAmount)) - .to.emit(usdc, 'Transfer') - .withArgs(userA.address, account.address, depositAmount) - expect(await usdc.balanceOf(account.address)).to.equal(depositAmount) - }) - - it('can natively deposit USDC and withdraw USDC', async () => { - const depositAmount = parse6decimal('7000') - await usdc.connect(userA).transfer(account.address, depositAmount) - expect(await usdc.balanceOf(account.address)).to.equal(depositAmount) - - await expect(account.withdraw(depositAmount, false)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, depositAmount) - expect(await usdc.balanceOf(account.address)).to.equal(0) - }) - - it('can natively deposit DSU and withdraw as USDC', async () => { - const depositAmount = utils.parseEther('8000') - await dsu.connect(userA).transfer(account.address, depositAmount) - expect(await dsu.balanceOf(account.address)).to.equal(depositAmount) - - expect(depositAmount.div(1e12)).to.equal(parse6decimal('8000')) - await expect(account.withdraw(depositAmount.div(1e12), true)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, parse6decimal('8000')) - expect(await dsu.balanceOf(account.address)).to.equal(0) - }) - - it('can withdraw all USDC without unwrapping DSU', async () => { - await dsu.connect(userA).transfer(account.address, utils.parseEther('300')) - await usdc.connect(userA).transfer(account.address, parse6decimal('400')) - - await expect(account.withdraw(parse6decimal('400'), false)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, parse6decimal('400')) - expect(await dsu.balanceOf(account.address)).to.equal(utils.parseEther('300')) - expect(await usdc.balanceOf(account.address)).to.equal(0) - }) - - it('can unwrap and withdraw everything', async () => { - await dsu.connect(userA).transfer(account.address, utils.parseEther('100')) - await usdc.connect(userA).transfer(account.address, parse6decimal('200')) - - await expect(account.withdraw(parse6decimal('300'), true)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, parse6decimal('300')) - expect(await dsu.balanceOf(account.address)).to.equal(0) - expect(await usdc.balanceOf(account.address)).to.equal(0) - }) - - it('unwraps only when necessary', async () => { - await dsu.connect(userA).transfer(account.address, utils.parseEther('600')) - await usdc.connect(userA).transfer(account.address, parse6decimal('700')) - - // should not unwrap when withdrawing less USDC than the account's balance - await expect(account.withdraw(parse6decimal('500'), true)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, parse6decimal('500')) - expect(await dsu.balanceOf(account.address)).to.equal(utils.parseEther('600')) - expect(await usdc.balanceOf(account.address)).to.equal(parse6decimal('200')) - - // should unwrap when withdrawing more than the account's balance (now 200 USDC) - await expect(account.withdraw(parse6decimal('300'), true)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, parse6decimal('300')) - expect(await dsu.balanceOf(account.address)).to.equal(utils.parseEther('500')) - expect(await usdc.balanceOf(account.address)).to.equal(0) - }) - - it('burns dust amounts upon withdrawal', async () => { - // deposit a dust amount into the account - const dust = utils.parseEther('0.000000555') - await dsu.connect(userA).transfer(account.address, dust) - expect(await usdc.balanceOf(account.address)).equals(constants.Zero) - expect(await dsu.balanceOf(account.address)).equals(dust) - - // amount is below the smallest transferrable amount of USDC, so nothing is transferred - await expect(account.withdraw(constants.MaxUint256, true)) - .to.emit(usdc, 'Transfer') - .withArgs(account.address, userA.address, 0) - - // ensure the withdrawal burned the DSU dust - expect(await usdc.balanceOf(account.address)).equals(constants.Zero) - expect(await dsu.balanceOf(account.address)).equals(constants.Zero) - }) - - it('transfer fails if insufficient balance when not unwrapping', async () => { - await dsu.connect(userA).transfer(account.address, utils.parseEther('100')) - expect(await usdc.balanceOf(account.address)).to.equal(0) - - // ensure withdrawal fails when there is no unwrapped USDC - await expect(account.withdraw(parse6decimal('100'), false)).to.be.revertedWith( - 'ERC20: transfer amount exceeds balance', - ) - - // and when there is some, but not enough to facilitate the withdrawal - await usdc.connect(userA).transfer(account.address, parse6decimal('50')) - await expect(account.withdraw(parse6decimal('100'), false)).to.be.revertedWith( - 'ERC20: transfer amount exceeds balance', - ) - }) - - it('transfer fails if insufficient balance when unwrapping', async () => { - await dsu.connect(userA).transfer(account.address, utils.parseEther('100')) - expect(await usdc.balanceOf(account.address)).to.equal(0) - - // ensure withdrawal fails when there is unsufficient DSU to unwrap - await expect(account.withdraw(parse6decimal('150'), true)).to.be.revertedWith( - 'ERC20: transfer amount exceeds balance', - ) +export function RunAccountTests( + deployProtocol: (owner: SignerWithAddress, overrides?: CallOverrides) => Promise, + deployInstance: ( + owner: SignerWithAddress, + marketFactory: IMarketFactory, + overrides?: CallOverrides, + ) => Promise, +): void { + describe('Account', () => { + let deployment: DeploymentVars + let dsu: IERC20Metadata + let usdc: IERC20Metadata + let controller: IController + let account: Account + let owner: SignerWithAddress + let userA: SignerWithAddress + let userB: SignerWithAddress + + // funds specified wallet with 50k DSU and 100k USDC + async function fundWallet(wallet: SignerWithAddress): Promise { + await deployment.fundWalletDSU(wallet, utils.parseEther('50000')) + await deployment.fundWalletUSDC(wallet, parse6decimal('100000')) + } + + const fixture = async () => { + ;[owner, userA, userB] = await ethers.getSigners() + deployment = await deployProtocol(owner) + dsu = deployment.dsu + usdc = deployment.usdc + controller = await deployInstance(owner, deployment.marketFactory) + + // fund users with some DSU and USDC + await fundWallet(userA) + await fundWallet(userB) + + // create an empty account + const accountAddress = await controller.connect(userA).callStatic.deployAccount() + await controller.connect(userA).deployAccount() + account = Account__factory.connect(accountAddress, userA) + } + + beforeEach(async () => { + await loadFixture(fixture) }) - it('reverts if someone other than the owner attempts a withdrawal', async () => { - await expect(account.connect(userB).withdraw(parse6decimal('400'), false)).to.be.revertedWithCustomError( - account, - 'AccountNotAuthorizedError', - ) + describe('#deposit and withdrawal', () => { + it('can use deposit function to pull USDC into account', async () => { + // run token approval + const depositAmount = parse6decimal('6000') + await usdc.connect(userA).approve(account.address, depositAmount) + + // call the deposit function to transferFrom userA + await expect(account.deposit(depositAmount)) + .to.emit(usdc, 'Transfer') + .withArgs(userA.address, account.address, depositAmount) + expect(await usdc.balanceOf(account.address)).to.equal(depositAmount) + }) + + it('can natively deposit USDC and withdraw USDC', async () => { + const depositAmount = parse6decimal('7000') + await usdc.connect(userA).transfer(account.address, depositAmount) + expect(await usdc.balanceOf(account.address)).to.equal(depositAmount) + + await expect(account.withdraw(depositAmount, false)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, depositAmount) + expect(await usdc.balanceOf(account.address)).to.equal(0) + }) + + it('can natively deposit DSU and withdraw as USDC', async () => { + const depositAmount = utils.parseEther('8000') + await dsu.connect(userA).transfer(account.address, depositAmount) + expect(await dsu.balanceOf(account.address)).to.equal(depositAmount) + + expect(depositAmount.div(1e12)).to.equal(parse6decimal('8000')) + await expect(account.withdraw(depositAmount.div(1e12), true)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, parse6decimal('8000')) + expect(await dsu.balanceOf(account.address)).to.equal(0) + }) + + it('can withdraw all USDC without unwrapping DSU', async () => { + await dsu.connect(userA).transfer(account.address, utils.parseEther('300')) + await usdc.connect(userA).transfer(account.address, parse6decimal('400')) + + await expect(account.withdraw(parse6decimal('400'), false)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, parse6decimal('400')) + expect(await dsu.balanceOf(account.address)).to.equal(utils.parseEther('300')) + expect(await usdc.balanceOf(account.address)).to.equal(0) + }) + + it('can unwrap and withdraw everything', async () => { + await dsu.connect(userA).transfer(account.address, utils.parseEther('100')) + await usdc.connect(userA).transfer(account.address, parse6decimal('200')) + + await expect(account.withdraw(parse6decimal('300'), true)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, parse6decimal('300')) + expect(await dsu.balanceOf(account.address)).to.equal(0) + expect(await usdc.balanceOf(account.address)).to.equal(0) + }) + + it('unwraps only when necessary', async () => { + await dsu.connect(userA).transfer(account.address, utils.parseEther('600')) + await usdc.connect(userA).transfer(account.address, parse6decimal('700')) + + // should not unwrap when withdrawing less USDC than the account's balance + await expect(account.withdraw(parse6decimal('500'), true)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, parse6decimal('500')) + expect(await dsu.balanceOf(account.address)).to.equal(utils.parseEther('600')) + expect(await usdc.balanceOf(account.address)).to.equal(parse6decimal('200')) + + // should unwrap when withdrawing more than the account's balance (now 200 USDC) + await expect(account.withdraw(parse6decimal('300'), true)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, parse6decimal('300')) + expect(await dsu.balanceOf(account.address)).to.equal(utils.parseEther('500')) + expect(await usdc.balanceOf(account.address)).to.equal(0) + }) + + it('burns dust amounts upon withdrawal', async () => { + // deposit a dust amount into the account + const dust = utils.parseEther('0.000000555') + await dsu.connect(userA).transfer(account.address, dust) + expect(await usdc.balanceOf(account.address)).equals(constants.Zero) + expect(await dsu.balanceOf(account.address)).equals(dust) + + // amount is below the smallest transferrable amount of USDC, so nothing is transferred + await expect(account.withdraw(constants.MaxUint256, true)) + .to.emit(usdc, 'Transfer') + .withArgs(account.address, userA.address, 0) + + // ensure the withdrawal burned the DSU dust + expect(await usdc.balanceOf(account.address)).equals(constants.Zero) + expect(await dsu.balanceOf(account.address)).equals(constants.Zero) + }) + + it('transfer fails if insufficient balance when not unwrapping', async () => { + await dsu.connect(userA).transfer(account.address, utils.parseEther('100')) + expect(await usdc.balanceOf(account.address)).to.equal(0) + + // ensure withdrawal fails when there is no unwrapped USDC + await expect(account.withdraw(parse6decimal('100'), false)).to.be.revertedWith( + 'ERC20: transfer amount exceeds balance', + ) + + // and when there is some, but not enough to facilitate the withdrawal + await usdc.connect(userA).transfer(account.address, parse6decimal('50')) + await expect(account.withdraw(parse6decimal('100'), false)).to.be.revertedWith( + 'ERC20: transfer amount exceeds balance', + ) + }) + + it('transfer fails if insufficient balance when unwrapping', async () => { + await dsu.connect(userA).transfer(account.address, utils.parseEther('100')) + expect(await usdc.balanceOf(account.address)).to.equal(0) + + // ensure withdrawal fails when there is unsufficient DSU to unwrap + await expect(account.withdraw(parse6decimal('150'), true)).to.be.revertedWith( + 'ERC20: transfer amount exceeds balance', + ) + }) + + it('reverts if someone other than the owner attempts a withdrawal', async () => { + await expect(account.connect(userB).withdraw(parse6decimal('400'), false)).to.be.revertedWithCustomError( + account, + 'AccountNotAuthorizedError', + ) + }) }) }) -}) +} diff --git a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts similarity index 72% rename from packages/perennial-account/test/integration/Controller_Arbitrum.test.ts rename to packages/perennial-account/test/integration/Arbitrum.test.ts index cddc9fffa..318437850 100644 --- a/packages/perennial-account/test/integration/Controller_Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -1,7 +1,6 @@ import { smock } from '@defi-wonderland/smock' import { use } from 'chai' import { CallOverrides } from 'ethers' -import HRE from 'hardhat' import { ArbGasInfo } from '../../types/generated' import { @@ -11,13 +10,16 @@ import { fundWalletUSDC, getStablecoins, } from '../helpers/arbitrumHelpers' -import { createMarketBTC, createMarketETH } from '../helpers/setupHelpers' -import { DeploymentVars, RunIncentivizedTests } from './Controller_Incentivized.test' +import { createMarketBTC, createMarketETH, DeploymentVars } from '../helpers/setupHelpers' +import { RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/generated' +import { Controller_Incentivized, IMarketFactory } from '../../types/generated' +import { RunAccountTests } from './Account.test' use(smock.matchers) +// TODO: Seems inelegant using this same implementation to call methods from a chain-specific helper library. +// But the helpers are destined to move to a common folder shareable across extensions. async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { const [oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) @@ -54,10 +56,9 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride async function deployInstance( owner: SignerWithAddress, marketFactory: IMarketFactory, - relayVerifier: IVerifier, overrides?: CallOverrides, ): Promise { - return deployControllerArbitrum(owner, marketFactory, relayVerifier, overrides) + return deployControllerArbitrum(owner, marketFactory, overrides) } async function mockGasInfo() { @@ -68,5 +69,7 @@ async function mockGasInfo() { gasInfo.getL1BaseFeeEstimate.returns(0) } -if (process.env.FORK_NETWORK === 'arbitrum') +if (process.env.FORK_NETWORK === 'arbitrum') { + RunAccountTests(deployProtocol, deployInstance) RunIncentivizedTests('Controller_Arbitrum', deployProtocol, deployInstance, mockGasInfo) +} diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index 8a44c8ddf..2ddde2f6d 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -20,6 +20,7 @@ import { import { IMarket, IMarketFactory } from '@equilibria/perennial-v2/types/generated' import { signDeployAccount, signMarketTransfer, signRebalanceConfigChange, signWithdrawal } from '../helpers/erc712' import { advanceToPrice, createMarketBTC, createMarketETH } from '../helpers/setupHelpers' +// TODO: replace with chain-agnostic implementations import { createFactoriesForChain, deployAndInitializeController, @@ -32,7 +33,7 @@ const { ethers } = HRE // hack around intermittent issues estimating gas const TX_OVERRIDES = { gasLimit: 3_000_000, maxFeePerGas: 200_000_000 } -describe.skip('ControllerBase', () => { +describe('ControllerBase', () => { let dsu: IERC20Metadata let usdc: IERC20Metadata let controller: Controller @@ -541,6 +542,11 @@ describe.skip('ControllerBase', () => { }) describe('#withdrawal', () => { + let usdcBalanceBefore: BigNumber + beforeEach(async () => { + usdcBalanceBefore = await usdc.balanceOf(userA.address) + }) + it('can unwrap and partially withdraw funds from a signed message', async () => { // sign message to perform a partial withdrawal const withdrawalAmount = parse6decimal('6000') @@ -557,7 +563,7 @@ describe.skip('ControllerBase', () => { .withArgs(accountA.address, userA.address, withdrawalAmount) // ensure owner was credited the USDC and account's DSU was debited - expect(await usdc.balanceOf(userA.address)).to.equal(withdrawalAmount) + expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.equal(withdrawalAmount) expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('9000')) // 15k-9k expect(await usdc.balanceOf(accountA.address)).to.equal(0) // no USDC was deposited }) @@ -578,7 +584,7 @@ describe.skip('ControllerBase', () => { await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature)).to.not.be.reverted // ensure owner was credit all the USDC and account is empty - expect(await usdc.balanceOf(userA.address)).to.equal(parse6decimal('15000')) + expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.equal(parse6decimal('15000')) expect(await dsu.balanceOf(accountA.address)).to.equal(0) // all DSU was withdrawan expect(await usdc.balanceOf(accountA.address)).to.equal(0) // no USDC was deposited }) diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index c3a2b1de3..cf1de455d 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -19,7 +19,6 @@ import { IERC20Metadata, IMarket, IMarketFactory, - IVerifier, } from '../../types/generated' import { @@ -33,7 +32,7 @@ import { signRelayedSignerUpdate, signWithdrawal, } from '../helpers/erc712' -import { advanceToPrice } from '../helpers/setupHelpers' +import { advanceToPrice, DeploymentVars } from '../helpers/setupHelpers' import { signAccessUpdateBatch, signGroupCancellation, @@ -42,8 +41,7 @@ import { signSignerUpdate, } from '@equilibria/perennial-v2-verifier/test/helpers/erc712' import { Verifier, Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' -import { IVerifier__factory } from '@equilibria/perennial-v2/types/generated' -import { IKeeperOracle, IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' +import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' const { ethers } = HRE @@ -53,27 +51,12 @@ const DEFAULT_MAX_FEE = parse6decimal('0.5') // hack around issues estimating gas for instrumented contracts when running tests under coverage const TX_OVERRIDES = { gasLimit: 3_000_000, maxPriorityFeePerGas: 0, maxFeePerGas: 100_000_000 } -export interface DeploymentVars { - dsu: IERC20Metadata - usdc: IERC20Metadata - oracleFactory: IOracleFactory - pythOracleFactory: PythFactory - marketFactory: IMarketFactory - ethMarket: IMarket - btcMarket: IMarket - ethKeeperOracle: IKeeperOracle - btcKeeperOracle: IKeeperOracle - fundWalletDSU(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise - fundWalletUSDC(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise -} - export function RunIncentivizedTests( name: string, deployProtocol: (owner: SignerWithAddress, overrides?: CallOverrides) => Promise, deployInstance: ( owner: SignerWithAddress, marketFactory: IMarketFactory, - relayVerifier: IVerifier, overrides?: CallOverrides, ) => Promise, mockGasInfo: () => Promise, @@ -195,9 +178,8 @@ export function RunIncentivizedTests( const fixture = async () => { // deploy the protocol ;[owner, userA, userB, userC, keeper, receiver] = await ethers.getSigners() - console.log('deployProtocol') deployment = await deployProtocol(owner, TX_OVERRIDES) - // TODO: consider replacing these 8 member variables with an instance of DeploymentVars + // TODO: eliminate infrequently used member variables dsu = deployment.dsu usdc = deployment.usdc marketFactory = deployment.marketFactory @@ -206,7 +188,6 @@ export function RunIncentivizedTests( ethKeeperOracle = deployment.ethKeeperOracle btcKeeperOracle = deployment.btcKeeperOracle - console.log('set initial prices') await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES) await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('57575.464'), TX_OVERRIDES) @@ -231,10 +212,7 @@ export function RunIncentivizedTests( multiplierCalldata: ethers.utils.parseEther('1.05'), bufferCalldata: 35_200, } - const marketVerifier = IVerifier__factory.connect(await marketFactory.verifier(), owner) - controller = await deployInstance(owner, marketFactory, marketVerifier, { - maxFeePerGas: 100000000, - }) + controller = await deployInstance(owner, marketFactory, { maxFeePerGas: 100000000 }) accountVerifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address, { maxFeePerGas: 100000000, }) @@ -679,12 +657,12 @@ export function RunIncentivizedTests( describe('#withdrawal', async () => { let accountA: Account - let userBalanceBefore: BigNumber + let usdcBalanceBefore: BigNumber beforeEach(async () => { // deploy collateral account for userA accountA = await createCollateralAccount(userA, parse6decimal('17000')) - userBalanceBefore = await usdc.balanceOf(userA.address) + usdcBalanceBefore = await usdc.balanceOf(userA.address) }) afterEach(async () => { @@ -714,7 +692,7 @@ export function RunIncentivizedTests( // confirm userA withdrew their funds and keeper fee was paid from the collateral account expect(await usdc.balanceOf(accountA.address)).to.be.within(parse6decimal('9999'), parse6decimal('10000')) - expect(await usdc.balanceOf(userA.address)).to.equal(userBalanceBefore.add(withdrawalAmount)) + expect(await usdc.balanceOf(userA.address)).to.equal(usdcBalanceBefore.add(withdrawalAmount)) }) it('collects fee for full withdrawal', async () => { @@ -738,7 +716,10 @@ export function RunIncentivizedTests( expect(await usdc.balanceOf(accountA.address)).to.equal(0) // user should have their initial balance, plus what was in their collateral account, minus keeper fees - expect(await usdc.balanceOf(userA.address)).to.be.within(parse6decimal('49999'), parse6decimal('50000')) + expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.be.within( + parse6decimal('16999'), + parse6decimal('17000'), + ) }) }) diff --git a/packages/perennial-account/test/integration/Controller_Optimism.test.ts b/packages/perennial-account/test/integration/Optimism.test.ts similarity index 71% rename from packages/perennial-account/test/integration/Controller_Optimism.test.ts rename to packages/perennial-account/test/integration/Optimism.test.ts index 88a20fc1b..bfa9b2b71 100644 --- a/packages/perennial-account/test/integration/Controller_Optimism.test.ts +++ b/packages/perennial-account/test/integration/Optimism.test.ts @@ -1,9 +1,7 @@ import { smock } from '@defi-wonderland/smock' -import { use } from 'chai' import { CallOverrides } from 'ethers' -import HRE from 'hardhat' -import { ArbGasInfo, OptGasInfo } from '../../types/generated' +import { OptGasInfo } from '../../types/generated' import { createFactoriesForChain, deployControllerOptimism, @@ -11,17 +9,15 @@ import { fundWalletUSDC, getStablecoins, } from '../helpers/baseHelpers' -import { createMarketBTC, createMarketETH } from '../helpers/setupHelpers' -import { DeploymentVars, RunIncentivizedTests } from './Controller_Incentivized.test' +import { createMarketBTC, createMarketETH, DeploymentVars } from '../helpers/setupHelpers' +import { RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { Controller_Incentivized, IMarketFactory, IVerifier } from '../../types/generated' +import { Controller_Incentivized, IMarketFactory } from '../../types/generated' +import { RunAccountTests } from './Account.test' async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { - console.log('createFactoriesForChain') const [oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) - console.log('getStablecoins') const [dsu, usdc] = await getStablecoins(owner) - console.log('createMarketETH') const [ethMarket, , ethKeeperOracle] = await createMarketETH( owner, oracleFactory, @@ -29,7 +25,6 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride marketFactory, dsu, ) - console.log('createMarketBTC') const [btcMarket, , btcKeeperOracle] = await createMarketBTC( owner, oracleFactory, @@ -56,10 +51,9 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride async function deployInstance( owner: SignerWithAddress, marketFactory: IMarketFactory, - relayVerifier: IVerifier, overrides?: CallOverrides, ): Promise { - return deployControllerOptimism(owner, marketFactory, relayVerifier, overrides) + return deployControllerOptimism(owner, marketFactory, overrides) } async function mockGasInfo() { @@ -73,5 +67,7 @@ async function mockGasInfo() { gasInfo.decimals.returns(6) } -if (process.env.FORK_NETWORK === 'base') +if (process.env.FORK_NETWORK === 'base') { + RunAccountTests(deployProtocol, deployInstance) RunIncentivizedTests('Controller_Optimism', deployProtocol, deployInstance, mockGasInfo) +} diff --git a/packages/perennial-order/test/helpers/arbitrumHelpers.ts b/packages/perennial-order/test/helpers/arbitrumHelpers.ts index 8742cba5c..d965ae36f 100644 --- a/packages/perennial-order/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-order/test/helpers/arbitrumHelpers.ts @@ -2,9 +2,7 @@ import { expect } from 'chai' import { BigNumber, CallOverrides, constants, utils } from 'ethers' import { IMarket, MarketFactory, MarketFactory__factory } from '@equilibria/perennial-v2/types/generated' import { - IKeeperOracle, IOracleFactory, - IOracleProvider, KeeperOracle__factory, OracleFactory, PythFactory, @@ -17,40 +15,14 @@ import { IERC20Metadata, IERC20Metadata__factory, IMarketFactory, IVerifier } fr import { impersonate } from '../../../common/testutil' import { Address } from 'hardhat-deploy/dist/types' import { parse6decimal } from '../../../common/testutil/types' -import { createPythOracle, deployOracleFactory } from './oracleHelpers' -import { createMarket, deployMarketImplementation } from './marketHelpers' +import { deployOracleFactory } from './oracleHelpers' +import { deployMarketImplementation } from './marketHelpers' const DSU_ADDRESS = '0x52C64b8998eB7C80b6F526E99E29ABdcC86B841b' // Digital Standard Unit, an 18-decimal token const DSU_HOLDER = '0x90a664846960aafa2c164605aebb8e9ac338f9a0' // Perennial Market has 4.7mm at height 243648015 const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' -const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' -// creates an ETH market using a locally deployed factory and oracle -export async function createMarketETH( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, - pythOracleFactory: PythFactory, - marketFactory: IMarketFactory, - dsu: IERC20Metadata, - overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { - // Create oracles needed to support the market - const [keeperOracle, oracle] = await createPythOracle( - owner, - oracleFactory, - pythOracleFactory, - PYTH_ETH_USD_PRICE_FEED, - 'ETH-USD', - overrides, - ) - // Create the market in which user or collateral account may interact - const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) - await keeperOracle.register(oracle.address) - await oracle.register(market.address) - return [market, oracle, keeperOracle] -} - // Deploys the market factory and configures default protocol parameters async function deployMarketFactory( owner: SignerWithAddress, From 5f94ede3ac3bb6c00f67bebe39f308207377cd14 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Wed, 16 Oct 2024 16:03:17 -0400 Subject: [PATCH 07/23] fixed issue with controller's kept oracle --- .../test/helpers/arbitrumHelpers.ts | 3 ++- .../test/helpers/baseHelpers.ts | 7 +++++-- .../test/helpers/setupHelpers.ts | 16 +++++++++++++--- .../test/integration/Arbitrum.test.ts | 3 ++- .../test/integration/Controller.test.ts | 2 +- .../integration/Controller_Incentivized.test.ts | 3 +-- .../test/integration/Optimism.test.ts | 3 ++- 7 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index b05d22d02..ad4f40e81 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -14,6 +14,7 @@ import { IMarketFactory, } from '../../types/generated' import { impersonate } from '../../../common/testutil' +import { AggregatorV3Interface } from '@equilibria/perennial-v2/types/generated' const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' @@ -28,7 +29,7 @@ const USDC_HOLDER = '0x2df1c51e09aecf9cacb7bc98cb1742757f163df7' // Hyperliquid // deploys protocol export async function createFactoriesForChain( owner: SignerWithAddress, -): Promise<[IOracleFactory, IMarketFactory, PythFactory]> { +): Promise<[IOracleFactory, IMarketFactory, PythFactory, AggregatorV3Interface]> { return createFactories(owner, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) } diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts index 3639ff586..32c95f670 100644 --- a/packages/perennial-account/test/helpers/baseHelpers.ts +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -6,6 +6,7 @@ import { createFactories, deployController } from './setupHelpers' import { Account__factory, AccountVerifier__factory, + AggregatorV3Interface, Controller, Controller_Optimism, Controller_Optimism__factory, @@ -17,7 +18,7 @@ import { import { impersonate } from '../../../common/testutil' const PYTH_ADDRESS = '0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a' -const CHAINLINK_ETH_USD_FEED = '0x4aDC67696bA383F43DD60A9e78F2C97Fbbfc7cb1' // TODO: confirm interface is same +const CHAINLINK_ETH_USD_FEED = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70' const DSU_ADDRESS = '0x7b4Adf64B0d60fF97D672E473420203D52562A84' // Digital Standard Unit, an 18-decimal token const DSU_RESERVE = '0x5FA881826AD000D010977645450292701bc2f56D' @@ -28,7 +29,7 @@ const USDC_HOLDER = '0xF977814e90dA44bFA03b6295A0616a897441aceC' // EOA has 302m // deploys protocol export async function createFactoriesForChain( owner: SignerWithAddress, -): Promise<[IOracleFactory, IMarketFactory, PythFactory]> { +): Promise<[IOracleFactory, IMarketFactory, PythFactory, AggregatorV3Interface]> { return createFactories(owner, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) } @@ -79,6 +80,8 @@ export async function fundWalletDSU( // fund wallet with USDC and then mint using reserve await fundWalletUSDC(wallet, amount.div(1e12), overrides) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, wallet) + await usdc.connect(wallet).approve(reserve.address, amount.div(1e12)) await reserve.mint(amount) expect((await dsu.balanceOf(wallet.address)).sub(balanceBefore)).to.equal(amount) diff --git a/packages/perennial-account/test/helpers/setupHelpers.ts b/packages/perennial-account/test/helpers/setupHelpers.ts index bcb9c1b8c..2377aa167 100644 --- a/packages/perennial-account/test/helpers/setupHelpers.ts +++ b/packages/perennial-account/test/helpers/setupHelpers.ts @@ -8,7 +8,13 @@ import { parse6decimal } from '../../../common/testutil/types' import { currentBlockTimestamp, increaseTo } from '../../../common/testutil/time' import { getTimestamp } from '../../../common/testutil/transaction' -import { Account__factory, Controller, Controller__factory, IERC20Metadata } from '../../types/generated' +import { + Account__factory, + AggregatorV3Interface, + Controller, + Controller__factory, + IERC20Metadata, +} from '../../types/generated' import { CheckpointLib__factory, CheckpointStorageLib__factory, @@ -46,6 +52,7 @@ import { PythFactory__factory, KeeperOracle, Oracle, + AggregatorV3Interface__factory, } from '@equilibria/perennial-v2-oracle/types/generated' import { OracleVersionStruct } from '../../types/generated/@equilibria/perennial-v2/contracts/interfaces/IOracleProvider' import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' @@ -65,6 +72,7 @@ export interface DeploymentVars { btcMarket: IMarket ethKeeperOracle: IKeeperOracle btcKeeperOracle: IKeeperOracle + chainlinkKeptFeed: AggregatorV3Interface fundWalletDSU(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise fundWalletUSDC(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise } @@ -106,11 +114,13 @@ export async function createFactories( owner: SignerWithAddress, pythAddress: Address, chainLinkFeedAddress: Address, -): Promise<[IOracleFactory, IMarketFactory, PythFactory]> { +): Promise<[IOracleFactory, IMarketFactory, PythFactory, AggregatorV3Interface]> { // Deploy the oracle factory, which markets created by the market factory will query const oracleFactory = await deployOracleFactory(owner) // Deploy the market factory and authorize it with the oracle factory const marketFactory = await deployProtocolForOracle(owner, oracleFactory) + // Connect the Chainlink ETH feed used for keeper compensation + const chainlinkKeptFeed = AggregatorV3Interface__factory.connect(chainLinkFeedAddress, owner) // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices const commitmentGasOracle = await new GasOracle__factory(owner).deploy( @@ -145,7 +155,7 @@ export async function createFactories( await pythOracleFactory.updateParameter(1, 0, 4, 10) await oracleFactory.register(pythOracleFactory.address) - return [oracleFactory, marketFactory, pythOracleFactory] + return [oracleFactory, marketFactory, pythOracleFactory, chainlinkKeptFeed] } // Using a provided factory, create a new market and set some reasonable initial parameters diff --git a/packages/perennial-account/test/integration/Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts index 318437850..f00e55688 100644 --- a/packages/perennial-account/test/integration/Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -21,7 +21,7 @@ use(smock.matchers) // TODO: Seems inelegant using this same implementation to call methods from a chain-specific helper library. // But the helpers are destined to move to a common folder shareable across extensions. async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { - const [oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) + const [oracleFactory, marketFactory, pythOracleFactory, chainlinkKeptFeed] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) const [ethMarket, , ethKeeperOracle] = await createMarketETH( owner, @@ -48,6 +48,7 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride btcMarket, ethKeeperOracle, btcKeeperOracle, + chainlinkKeptFeed, fundWalletDSU, fundWalletUSDC, } diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index 2ddde2f6d..ba8b4dc90 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -33,7 +33,7 @@ const { ethers } = HRE // hack around intermittent issues estimating gas const TX_OVERRIDES = { gasLimit: 3_000_000, maxFeePerGas: 200_000_000 } -describe('ControllerBase', () => { +describe.skip('ControllerBase', () => { let dsu: IERC20Metadata let usdc: IERC20Metadata let controller: Controller diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index cf1de455d..d6dd2b898 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -45,7 +45,6 @@ import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' const { ethers } = HRE -const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' // price feed used for keeper compensation const DEFAULT_MAX_FEE = parse6decimal('0.5') // hack around issues estimating gas for instrumented contracts when running tests under coverage @@ -220,7 +219,7 @@ export function RunIncentivizedTests( const KeepConfig = '(uint256,uint256,uint256,uint256)' await controller[`initialize(address,address,${KeepConfig},${KeepConfig},${KeepConfig})`]( accountVerifier.address, - CHAINLINK_ETH_USD_FEED, + deployment.chainlinkKeptFeed.address, keepConfig, keepConfigBuffered, keepConfigWithdrawal, diff --git a/packages/perennial-account/test/integration/Optimism.test.ts b/packages/perennial-account/test/integration/Optimism.test.ts index bfa9b2b71..cecaba634 100644 --- a/packages/perennial-account/test/integration/Optimism.test.ts +++ b/packages/perennial-account/test/integration/Optimism.test.ts @@ -16,7 +16,7 @@ import { Controller_Incentivized, IMarketFactory } from '../../types/generated' import { RunAccountTests } from './Account.test' async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { - const [oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) + const [oracleFactory, marketFactory, pythOracleFactory, chainlinkKeptFeed] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) const [ethMarket, , ethKeeperOracle] = await createMarketETH( owner, @@ -43,6 +43,7 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride btcMarket, ethKeeperOracle, btcKeeperOracle, + chainlinkKeptFeed, fundWalletDSU, fundWalletUSDC, } From 853092fa0c8374d391015807c41a0d2d53213297 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Wed, 16 Oct 2024 23:11:13 -0400 Subject: [PATCH 08/23] made gas config chain-specific, balanced optimism gas params --- .../test/helpers/arbitrumHelpers.ts | 2 +- .../test/helpers/baseHelpers.ts | 2 +- .../test/integration/Account.test.ts | 7 ++- .../test/integration/Arbitrum.test.ts | 46 +++++++++++++++-- .../Controller_Incentivized.test.ts | 49 +++++------------- .../test/integration/Optimism.test.ts | 50 ++++++++++++++++--- 6 files changed, 106 insertions(+), 50 deletions(-) diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index ad4f40e81..c520aec19 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -33,7 +33,7 @@ export async function createFactoriesForChain( return createFactories(owner, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) } -// connects to Arbitrum stablecoins and deploys a controller configured for them +// connects to Arbitrum stablecoins and deploys a non-incentivized controller configured for them export async function deployAndInitializeController( owner: SignerWithAddress, marketFactory: IMarketFactory, diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts index 32c95f670..d951bfc76 100644 --- a/packages/perennial-account/test/helpers/baseHelpers.ts +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -33,7 +33,7 @@ export async function createFactoriesForChain( return createFactories(owner, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) } -// connects to Base stablecoins and deploys a controller configured for them +// connects to Base stablecoins and deploys a non-incentivized controller configured for them export async function deployAndInitializeController( owner: SignerWithAddress, marketFactory: IMarketFactory, diff --git a/packages/perennial-account/test/integration/Account.test.ts b/packages/perennial-account/test/integration/Account.test.ts index d82e5afe6..3cb627db9 100644 --- a/packages/perennial-account/test/integration/Account.test.ts +++ b/packages/perennial-account/test/integration/Account.test.ts @@ -7,7 +7,9 @@ import { parse6decimal } from '../../../common/testutil/types' import { Account, Account__factory, + AggregatorV3Interface, Controller_Incentivized, + IAccountVerifier, IController, IERC20Metadata, IMarketFactory, @@ -21,8 +23,9 @@ export function RunAccountTests( deployInstance: ( owner: SignerWithAddress, marketFactory: IMarketFactory, + chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, - ) => Promise, + ) => Promise<[Controller_Incentivized, IAccountVerifier]>, ): void { describe('Account', () => { let deployment: DeploymentVars @@ -45,7 +48,7 @@ export function RunAccountTests( deployment = await deployProtocol(owner) dsu = deployment.dsu usdc = deployment.usdc - controller = await deployInstance(owner, deployment.marketFactory) + ;[controller] = await deployInstance(owner, deployment.marketFactory, deployment.chainlinkKeptFeed) // fund users with some DSU and USDC await fundWallet(userA) diff --git a/packages/perennial-account/test/integration/Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts index f00e55688..b3e75f168 100644 --- a/packages/perennial-account/test/integration/Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -1,8 +1,9 @@ +import HRE from 'hardhat' import { smock } from '@defi-wonderland/smock' import { use } from 'chai' import { CallOverrides } from 'ethers' -import { ArbGasInfo } from '../../types/generated' +import { AccountVerifier__factory, ArbGasInfo, IAccountVerifier } from '../../types/generated' import { createFactoriesForChain, deployControllerArbitrum, @@ -15,6 +16,9 @@ import { RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory } from '../../types/generated' import { RunAccountTests } from './Account.test' +import { AggregatorV3Interface } from '@equilibria/perennial-v2-oracle/types/generated' + +const { ethers } = HRE use(smock.matchers) @@ -57,9 +61,45 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride async function deployInstance( owner: SignerWithAddress, marketFactory: IMarketFactory, + chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, -): Promise { - return deployControllerArbitrum(owner, marketFactory, overrides) +): Promise<[Controller_Incentivized, IAccountVerifier]> { + // FIXME: erroring with "trying to deploy a contract whose code is too large" when I pass empty overrides + const controller = await deployControllerArbitrum(owner, marketFactory /*, overrides ?? {}*/) + + const keepConfig = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 275_000, // buffer for handling the keeper fee + multiplierCalldata: ethers.utils.parseEther('1'), + bufferCalldata: 0, + } + const keepConfigBuffered = { + multiplierBase: ethers.utils.parseEther('1.08'), + bufferBase: 1_500_000, // for price commitment + multiplierCalldata: ethers.utils.parseEther('1.08'), + bufferCalldata: 35_200, + } + const keepConfigWithdrawal = { + multiplierBase: ethers.utils.parseEther('1.05'), + bufferBase: 1_500_000, + multiplierCalldata: ethers.utils.parseEther('1.05'), + bufferCalldata: 35_200, + } + + const accountVerifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address, { + maxFeePerGas: 100000000, + }) + // chainlink feed is used by Kept for keeper compensation + const KeepConfig = '(uint256,uint256,uint256,uint256)' + await controller[`initialize(address,address,${KeepConfig},${KeepConfig},${KeepConfig})`]( + accountVerifier.address, + chainlinkKeptFeed.address, + keepConfig, + keepConfigBuffered, + keepConfigWithdrawal, + ) + + return [controller, accountVerifier] } async function mockGasInfo() { diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index d6dd2b898..6b20a6982 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -12,7 +12,6 @@ import { parse6decimal } from '../../../common/testutil/types' import { Account, Account__factory, - AccountVerifier__factory, Controller_Incentivized, IAccount, IAccountVerifier, @@ -41,7 +40,7 @@ import { signSignerUpdate, } from '@equilibria/perennial-v2-verifier/test/helpers/erc712' import { Verifier, Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' -import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' +import { AggregatorV3Interface, IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' const { ethers } = HRE @@ -56,8 +55,9 @@ export function RunIncentivizedTests( deployInstance: ( owner: SignerWithAddress, marketFactory: IMarketFactory, + chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, - ) => Promise, + ) => Promise<[Controller_Incentivized, IAccountVerifier]>, mockGasInfo: () => Promise, ): void { describe(name, () => { @@ -131,6 +131,8 @@ export function RunIncentivizedTests( let keeperEthSpentOnGas = keeperEthBalanceBefore.sub(await keeper.getBalance()) // if TXes in test required outside price commitments, compensate the keeper for them + // TODO: This amount is for an Arbitrum price committment; should make this chain-specific + // once we know the cost of a Base price committment. keeperEthSpentOnGas = keeperEthSpentOnGas.add(utils.parseEther('0.0000644306').mul(priceCommitments)) // cost of transaction @@ -186,45 +188,18 @@ export function RunIncentivizedTests( btcMarket = deployment.btcMarket ethKeeperOracle = deployment.ethKeeperOracle btcKeeperOracle = deployment.btcKeeperOracle + ;[controller, accountVerifier] = await deployInstance( + owner, + deployment.marketFactory, + deployment.chainlinkKeptFeed, + TX_OVERRIDES, + ) await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES) await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('57575.464'), TX_OVERRIDES) - await dsu.connect(userA).approve(ethMarket.address, constants.MaxUint256, { maxFeePerGas: 100000000 }) - - // set up users and deploy artifacts - const keepConfig = { - multiplierBase: ethers.utils.parseEther('1'), - bufferBase: 275_000, // buffer for handling the keeper fee - multiplierCalldata: ethers.utils.parseEther('1'), - bufferCalldata: 0, - } - const keepConfigBuffered = { - multiplierBase: ethers.utils.parseEther('1.08'), - bufferBase: 1_500_000, // for price commitment - multiplierCalldata: ethers.utils.parseEther('1.08'), - bufferCalldata: 35_200, - } - const keepConfigWithdrawal = { - multiplierBase: ethers.utils.parseEther('1.05'), - bufferBase: 1_500_000, - multiplierCalldata: ethers.utils.parseEther('1.05'), - bufferCalldata: 35_200, - } - controller = await deployInstance(owner, marketFactory, { maxFeePerGas: 100000000 }) - accountVerifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address, { - maxFeePerGas: 100000000, - }) - // chainlink feed is used by Kept for keeper compensation - const KeepConfig = '(uint256,uint256,uint256,uint256)' - await controller[`initialize(address,address,${KeepConfig},${KeepConfig},${KeepConfig})`]( - accountVerifier.address, - deployment.chainlinkKeptFeed.address, - keepConfig, - keepConfigBuffered, - keepConfigWithdrawal, - ) // fund userA + await dsu.connect(userA).approve(ethMarket.address, constants.MaxUint256, { maxFeePerGas: 100000000 }) await deployment.fundWalletUSDC(userA, parse6decimal('50000'), { maxFeePerGas: 100000000 }) } diff --git a/packages/perennial-account/test/integration/Optimism.test.ts b/packages/perennial-account/test/integration/Optimism.test.ts index cecaba634..02dbc7cd0 100644 --- a/packages/perennial-account/test/integration/Optimism.test.ts +++ b/packages/perennial-account/test/integration/Optimism.test.ts @@ -1,7 +1,8 @@ +import HRE from 'hardhat' import { smock } from '@defi-wonderland/smock' import { CallOverrides } from 'ethers' -import { OptGasInfo } from '../../types/generated' +import { AccountVerifier__factory, AggregatorV3Interface, OptGasInfo } from '../../types/generated' import { createFactoriesForChain, deployControllerOptimism, @@ -15,6 +16,8 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory } from '../../types/generated' import { RunAccountTests } from './Account.test' +const { ethers } = HRE + async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { const [oracleFactory, marketFactory, pythOracleFactory, chainlinkKeptFeed] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) @@ -52,19 +55,54 @@ async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverride async function deployInstance( owner: SignerWithAddress, marketFactory: IMarketFactory, + chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, ): Promise { - return deployControllerOptimism(owner, marketFactory, overrides) + // FIXME: erroring with "trying to deploy a contract whose code is too large" when I pass empty overrides + const controller = await deployControllerOptimism(owner, marketFactory /*, overrides ?? {}*/) + + const keepConfig = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 0, // buffer for handling the keeper fee + multiplierCalldata: ethers.utils.parseEther('1'), + bufferCalldata: 0, + } + const keepConfigBuffered = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 2_000_000, // for price commitment + multiplierCalldata: ethers.utils.parseEther('1'), + bufferCalldata: 0, + } + const keepConfigWithdrawal = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 1_500_000, + multiplierCalldata: ethers.utils.parseEther('1'), + bufferCalldata: 0, + } + + const accountVerifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address, { + maxFeePerGas: 100000000, + }) + // chainlink feed is used by Kept for keeper compensation + const KeepConfig = '(uint256,uint256,uint256,uint256)' + await controller[`initialize(address,address,${KeepConfig},${KeepConfig},${KeepConfig})`]( + accountVerifier.address, + chainlinkKeptFeed.address, + keepConfig, + keepConfigBuffered, + keepConfigWithdrawal, + ) + + return [controller, accountVerifier] } async function mockGasInfo() { const gasInfo = await smock.fake('OptGasInfo', { address: '0x420000000000000000000000000000000000000F', }) - gasInfo.getL1GasUsed.returns(0) - gasInfo.getL1GasUsed.returns(0) - gasInfo.l1BaseFee.returns(0) - gasInfo.baseFeeScalar.returns(684000) + gasInfo.getL1GasUsed.returns(2000) + gasInfo.l1BaseFee.returns(3000000000) + gasInfo.baseFeeScalar.returns(5214379) gasInfo.decimals.returns(6) } From abb290ffffa0e9042e147b81d880a3c82fc78f13 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Thu, 17 Oct 2024 14:16:45 -0400 Subject: [PATCH 09/23] integration tests now run on both forks --- .../test/helpers/arbitrumHelpers.ts | 5 + .../test/helpers/baseHelpers.ts | 16 +- .../test/helpers/setupHelpers.ts | 22 +- .../test/integration/Account.test.ts | 9 +- .../test/integration/Arbitrum.test.ts | 51 +- .../test/integration/Controller.test.ts | 1131 +++++++++-------- .../Controller_Incentivized.test.ts | 34 +- .../test/integration/Optimism.test.ts | 56 +- 8 files changed, 690 insertions(+), 634 deletions(-) diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index c520aec19..7f301e94a 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -9,6 +9,7 @@ import { Controller, Controller_Arbitrum, Controller_Arbitrum__factory, + IEmptySetReserve__factory, IERC20Metadata, IERC20Metadata__factory, IMarketFactory, @@ -87,6 +88,10 @@ export async function fundWalletUSDC( await usdc.transfer(wallet.address, amount, overrides ?? {}) } +export function getDSUReserve(owner: SignerWithAddress) { + return IEmptySetReserve__factory.connect(DSU_RESERVE, owner) +} + export async function getStablecoins(owner: SignerWithAddress): Promise<[IERC20Metadata, IERC20Metadata]> { const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts index d951bfc76..22828fe89 100644 --- a/packages/perennial-account/test/helpers/baseHelpers.ts +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -46,12 +46,6 @@ export async function deployAndInitializeController( return [dsu, usdc, controller] } -export async function getStablecoins(owner: SignerWithAddress): Promise<[IERC20Metadata, IERC20Metadata]> { - const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) - const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) - return [dsu, usdc] -} - // deploys an instance of the Controller with keeper compensation mechanisms for OP stack chains export async function deployControllerOptimism( owner: SignerWithAddress, @@ -98,3 +92,13 @@ export async function fundWalletUSDC( expect(await usdc.balanceOf(USDC_HOLDER)).to.be.greaterThan(amount) await usdc.transfer(wallet.address, amount, overrides ?? {}) } + +export function getDSUReserve(owner: SignerWithAddress) { + return IEmptySetReserve__factory.connect(DSU_RESERVE, owner) +} + +export async function getStablecoins(owner: SignerWithAddress): Promise<[IERC20Metadata, IERC20Metadata]> { + const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) + return [dsu, usdc] +} diff --git a/packages/perennial-account/test/helpers/setupHelpers.ts b/packages/perennial-account/test/helpers/setupHelpers.ts index 2377aa167..95a8b683d 100644 --- a/packages/perennial-account/test/helpers/setupHelpers.ts +++ b/packages/perennial-account/test/helpers/setupHelpers.ts @@ -13,6 +13,7 @@ import { AggregatorV3Interface, Controller, Controller__factory, + IEmptySetReserve, IERC20Metadata, } from '../../types/generated' import { @@ -68,15 +69,20 @@ export interface DeploymentVars { oracleFactory: IOracleFactory pythOracleFactory: PythFactory marketFactory: IMarketFactory - ethMarket: IMarket - btcMarket: IMarket - ethKeeperOracle: IKeeperOracle - btcKeeperOracle: IKeeperOracle + ethMarket: MarketWithOracle | undefined + btcMarket: MarketWithOracle | undefined chainlinkKeptFeed: AggregatorV3Interface + dsuReserve: IEmptySetReserve fundWalletDSU(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise fundWalletUSDC(wallet: SignerWithAddress, amount: BigNumber, overrides?: CallOverrides): Promise } +export interface MarketWithOracle { + market: IMarket + oracle: IOracleProvider + keeperOracle: IKeeperOracle +} + // Simulates an oracle update from KeeperOracle. // If timestamp matches a requested version, callbacks implicitly settle the market. export async function advanceToPrice( @@ -239,7 +245,7 @@ export async function createMarketETH( marketFactory: IMarketFactory, dsu: IERC20Metadata, overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { +): Promise { // Create oracles needed to support the market const [keeperOracle, oracle] = await createPythOracle( owner, @@ -253,7 +259,7 @@ export async function createMarketETH( const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) await keeperOracle.register(oracle.address) await oracle.register(market.address) - return [market, oracle, keeperOracle] + return { market, oracle, keeperOracle } } // creates a BTC market using a locally deployed factory and oracle @@ -264,7 +270,7 @@ export async function createMarketBTC( marketFactory: IMarketFactory, dsu: IERC20Metadata, overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { +): Promise { // Create oracles needed to support the market const [keeperOracle, oracle] = await createPythOracle( owner, @@ -278,7 +284,7 @@ export async function createMarketBTC( const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) await keeperOracle.register(oracle.address) await oracle.register(market.address) - return [market, oracle, keeperOracle] + return { market, oracle, keeperOracle } } export async function createPythOracle( diff --git a/packages/perennial-account/test/integration/Account.test.ts b/packages/perennial-account/test/integration/Account.test.ts index 3cb627db9..301e96609 100644 --- a/packages/perennial-account/test/integration/Account.test.ts +++ b/packages/perennial-account/test/integration/Account.test.ts @@ -19,7 +19,12 @@ import { DeploymentVars } from '../helpers/setupHelpers' const { ethers } = HRE export function RunAccountTests( - deployProtocol: (owner: SignerWithAddress, overrides?: CallOverrides) => Promise, + deployProtocol: ( + owner: SignerWithAddress, + createMarketETH: boolean, + createMarketBTC: boolean, + overrides?: CallOverrides, + ) => Promise, deployInstance: ( owner: SignerWithAddress, marketFactory: IMarketFactory, @@ -45,7 +50,7 @@ export function RunAccountTests( const fixture = async () => { ;[owner, userA, userB] = await ethers.getSigners() - deployment = await deployProtocol(owner) + deployment = await deployProtocol(owner, false, false) dsu = deployment.dsu usdc = deployment.usdc ;[controller] = await deployInstance(owner, deployment.marketFactory, deployment.chainlinkKeptFeed) diff --git a/packages/perennial-account/test/integration/Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts index b3e75f168..5bd146c30 100644 --- a/packages/perennial-account/test/integration/Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -9,14 +9,20 @@ import { deployControllerArbitrum, fundWalletDSU, fundWalletUSDC, + getDSUReserve, getStablecoins, } from '../helpers/arbitrumHelpers' -import { createMarketBTC, createMarketETH, DeploymentVars } from '../helpers/setupHelpers' +import { + createMarketBTC as setupMarketBTC, + createMarketETH as setupMarketETH, + DeploymentVars, +} from '../helpers/setupHelpers' import { RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory } from '../../types/generated' import { RunAccountTests } from './Account.test' import { AggregatorV3Interface } from '@equilibria/perennial-v2-oracle/types/generated' +import { RunControllerBaseTests } from './Controller.test' const { ethers } = HRE @@ -24,38 +30,38 @@ use(smock.matchers) // TODO: Seems inelegant using this same implementation to call methods from a chain-specific helper library. // But the helpers are destined to move to a common folder shareable across extensions. -async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { +async function deployProtocol( + owner: SignerWithAddress, + createMarketETH = false, + createMarketBTC = false, + overrides?: CallOverrides, +): Promise { const [oracleFactory, marketFactory, pythOracleFactory, chainlinkKeptFeed] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) - const [ethMarket, , ethKeeperOracle] = await createMarketETH( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - ) - const [btcMarket, , btcKeeperOracle] = await createMarketBTC( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - overrides, - ) - return { + + const deployment: DeploymentVars = { dsu, usdc, oracleFactory, pythOracleFactory, marketFactory, - ethMarket, - btcMarket, - ethKeeperOracle, - btcKeeperOracle, + ethMarket: undefined, // TODO: style: inlining these was difficult to read; set below + btcMarket: undefined, chainlinkKeptFeed, + dsuReserve: getDSUReserve(owner), fundWalletDSU, fundWalletUSDC, } + + if (createMarketETH) { + deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + } + + if (createMarketBTC) { + deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + } + + return deployment } async function deployInstance( @@ -112,5 +118,6 @@ async function mockGasInfo() { if (process.env.FORK_NETWORK === 'arbitrum') { RunAccountTests(deployProtocol, deployInstance) + RunControllerBaseTests(deployProtocol) RunIncentivizedTests('Controller_Arbitrum', deployProtocol, deployInstance, mockGasInfo) } diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index ba8b4dc90..e7ece8ae9 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -2,608 +2,617 @@ import HRE from 'hardhat' import { expect } from 'chai' import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' import { Address } from 'hardhat-deploy/dist/types' -import { BigNumber, constants, utils } from 'ethers' +import { BigNumber, CallOverrides, constants, utils } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { currentBlockTimestamp } from '../../../common/testutil/time' import { getEventArguments } from '../../../common/testutil/transaction' import { parse6decimal } from '../../../common/testutil/types' -import { Account, Account__factory, Controller, IERC20Metadata } from '../../types/generated' +import { Account, Account__factory, AccountVerifier__factory, Controller, IERC20Metadata } from '../../types/generated' import { IAccountVerifier } from '../../types/generated/contracts/interfaces' -import { IAccountVerifier__factory } from '../../types/generated/factories/contracts/interfaces' -import { - IKeeperOracle, - IOracleFactory, - IOracleProvider, - PythFactory, -} from '@equilibria/perennial-v2-oracle/types/generated' import { IMarket, IMarketFactory } from '@equilibria/perennial-v2/types/generated' import { signDeployAccount, signMarketTransfer, signRebalanceConfigChange, signWithdrawal } from '../helpers/erc712' -import { advanceToPrice, createMarketBTC, createMarketETH } from '../helpers/setupHelpers' -// TODO: replace with chain-agnostic implementations -import { - createFactoriesForChain, - deployAndInitializeController, - fundWalletDSU, - fundWalletUSDC, -} from '../helpers/arbitrumHelpers' +import { advanceToPrice, deployController, DeploymentVars } from '../helpers/setupHelpers' const { ethers } = HRE // hack around intermittent issues estimating gas const TX_OVERRIDES = { gasLimit: 3_000_000, maxFeePerGas: 200_000_000 } -describe.skip('ControllerBase', () => { - let dsu: IERC20Metadata - let usdc: IERC20Metadata - let controller: Controller - let verifier: IAccountVerifier - let oracleFactory: IOracleFactory - let pythOracleFactory: PythFactory - let marketFactory: IMarketFactory - let ethMarket: IMarket - let ethKeeperOracle: IKeeperOracle - let accountA: Account - let owner: SignerWithAddress - let userA: SignerWithAddress - let userB: SignerWithAddress - let keeper: SignerWithAddress - let receiver: SignerWithAddress - let lastNonce = 0 - let lastPrice: BigNumber - let currentTime: BigNumber - - // create a default action for the specified user with reasonable fee and expiry - function createAction( - userAddress: Address, - signerAddress = userAddress, - maxFee = utils.parseEther('14'), - expiresInSeconds = 60, - ) { - return { - action: { - maxFee: maxFee, - common: { - account: userAddress, - signer: signerAddress, - domain: controller.address, - nonce: nextNonce(), - group: 0, - expiry: currentTime.add(expiresInSeconds), +export function RunControllerBaseTests( + deployProtocol: ( + owner: SignerWithAddress, + createMarketETH: boolean, + createMarketBTC: boolean, + overrides?: CallOverrides, + ) => Promise, +): void { + describe('ControllerBase', () => { + let deployment: DeploymentVars + let dsu: IERC20Metadata + let usdc: IERC20Metadata + let controller: Controller + let verifier: IAccountVerifier + let marketFactory: IMarketFactory + let ethMarket: IMarket + let accountA: Account + let owner: SignerWithAddress + let userA: SignerWithAddress + let userB: SignerWithAddress + let keeper: SignerWithAddress + let receiver: SignerWithAddress + let lastNonce = 0 + let lastPrice: BigNumber + let currentTime: BigNumber + + // create a default action for the specified user with reasonable fee and expiry + function createAction( + userAddress: Address, + signerAddress = userAddress, + maxFee = utils.parseEther('14'), + expiresInSeconds = 60, + ) { + return { + action: { + maxFee: maxFee, + common: { + account: userAddress, + signer: signerAddress, + domain: controller.address, + nonce: nextNonce(), + group: 0, + expiry: currentTime.add(expiresInSeconds), + }, }, - }, - } - } - - // updates the oracle (optionally changing price) and settles the market - async function advanceAndSettle( - user: SignerWithAddress, - receiver: SignerWithAddress, - timestamp = currentTime, - price = lastPrice, - keeperOracle = ethKeeperOracle, - ) { - await advanceToPrice(keeperOracle, receiver, timestamp, price, TX_OVERRIDES) - await ethMarket.settle(user.address, TX_OVERRIDES) - } - - // ensures user has expected amount of collateral in a market - async function expectMarketCollateralBalance(user: SignerWithAddress, amount: BigNumber) { - const local = await ethMarket.locals(user.address) - expect(local.collateral).to.equal(amount) - } - - // funds specified wallet with 50k collateral - async function fundWallet(wallet: SignerWithAddress): Promise { - await fundWalletDSU(wallet, utils.parseEther('50000')) - } - - // create a serial nonce for testing purposes; real users may choose a nonce however they please - function nextNonce(): BigNumber { - lastNonce += 1 - return BigNumber.from(lastNonce) - } - - // updates the market and returns the version timestamp - async function changePosition( - user: SignerWithAddress, - newMaker = constants.MaxUint256, - newLong = constants.MaxUint256, - newShort = constants.MaxUint256, - ): Promise { - const tx = await ethMarket - .connect(user) - ['update(address,uint256,uint256,uint256,int256,bool)']( - user.address, - newMaker, - newLong, - newShort, - 0, - false, - TX_OVERRIDES, - ) - return (await getEventArguments(tx, 'OrderCreated')).order.timestamp - } - - // performs a market transfer, returning the timestamp of the order produced - async function transfer( - amount: BigNumber, - user: SignerWithAddress, - market = ethMarket, - signer = user, - ): Promise { - const marketTransferMessage = { - market: market.address, - amount: amount, - ...createAction(user.address, signer.address), - } - const signature = await signMarketTransfer(signer, verifier, marketTransferMessage) - - // determine expected event parameters - let expectedFrom: Address, expectedTo: Address, expectedAmount: BigNumber - if (amount.gt(constants.Zero)) { - // deposits transfer from collateral account into market - expectedFrom = accountA.address - expectedTo = market.address - if (amount === constants.MaxInt256) expectedAmount = await dsu.balanceOf(accountA.address) - else expectedAmount = amount.mul(1e12) - } else { - // withdrawals transfer from market into account - expectedFrom = market.address - expectedTo = accountA.address - if (amount === constants.MinInt256) expectedAmount = (await market.locals(user.address)).collateral.mul(1e12) - else expectedAmount = amount.mul(-1e12) + } } - // perform transfer - await expect( - await controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ) - .to.emit(dsu, 'Transfer') - .withArgs(expectedFrom, expectedTo, expectedAmount) - .to.emit(market, 'OrderCreated') - .withArgs(userA.address, anyValue, anyValue, constants.AddressZero, constants.AddressZero, constants.AddressZero) - - const order = await market.pendingOrders(user.address, (await market.global()).currentId) - return order.timestamp - } - - const fixture = async () => { - // set up users - ;[owner, userA, userB, keeper, receiver] = await ethers.getSigners() - - // deploy controller - ;[oracleFactory, marketFactory, pythOracleFactory] = await createFactoriesForChain(owner) - ;[dsu, usdc, controller] = await deployAndInitializeController(owner, marketFactory) - verifier = IAccountVerifier__factory.connect(await controller.verifier(), owner) - - // create oracle, market, and set initial price - let oracle: IOracleProvider - ;[ethMarket, oracle, ethKeeperOracle] = await createMarketETH( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - TX_OVERRIDES, - ) - await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3116.734999'), TX_OVERRIDES) - lastPrice = (await oracle.status())[0].price - - // create a collateral account for userA with 15k collateral in it - await fundWallet(userA) - const accountAddressA = await controller.getAccountAddress(userA.address) - await dsu.connect(userA).transfer(accountAddressA, utils.parseEther('15000')) - const deployAccountMessage = { - ...createAction(userA.address), + // updates the oracle (optionally changing price) and settles the market + async function advanceAndSettle( + user: SignerWithAddress, + receiver: SignerWithAddress, + timestamp = currentTime, + price = lastPrice, + keeperOracle = deployment.ethMarket!.keeperOracle, + ) { + await advanceToPrice(keeperOracle, receiver, timestamp, price, TX_OVERRIDES) + await ethMarket.settle(user.address, TX_OVERRIDES) } - const signature = await signDeployAccount(userA, verifier, deployAccountMessage) - await controller.connect(keeper).deployAccountWithSignature(deployAccountMessage, signature) - accountA = Account__factory.connect(accountAddressA, userA) - - // approve the collateral account as operator - await marketFactory.connect(userA).updateOperator(accountA.address, true) - } - - beforeEach(async () => { - currentTime = BigNumber.from(await currentBlockTimestamp()) - await loadFixture(fixture) - }) - - describe('#rebalance', () => { - let btcMarket: IMarket - - beforeEach(async () => { - // create another market, including requisite oracles, and set initial price - let btcKeeperOracle - ;[btcMarket, , btcKeeperOracle] = await createMarketBTC( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - ) - await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('60606.369'), TX_OVERRIDES) - - // configure a group with both markets - const message = { - group: 1, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('0.65'), threshold: parse6decimal('0.04') }, - { target: parse6decimal('0.35'), threshold: parse6decimal('0.03') }, - ], - maxFee: constants.Zero, - ...(await createAction(userA.address)), - } - const signature = await signRebalanceConfigChange(userA, verifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be.reverted - }) - - it('checks a group within targets', async () => { - // transfer funds to the markets - await transfer(parse6decimal('9700'), userA, ethMarket) - await transfer(parse6decimal('5000'), userA, btcMarket) - - // check the group - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(parse6decimal('14700')) - expect(canRebalance).to.be.false - }) - - it('checks a group outside of targets', async () => { - // transfer funds to the markets - await transfer(parse6decimal('5000'), userA, ethMarket) - await transfer(parse6decimal('5000'), userA, btcMarket) - - // check the group - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(parse6decimal('10000')) - expect(canRebalance).to.be.true - }) - - it('should not rebalance an already-balanced group', async () => { - // transfer funds to the markets - await transfer(parse6decimal('9700'), userA, ethMarket) - await transfer(parse6decimal('5000'), userA, btcMarket) - - // attempt rebalance - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)).to.be.revertedWithCustomError( - controller, - 'ControllerGroupBalancedError', - ) - }) - - it('rebalances group outside of threshold', async () => { - // transfer funds to the markets - await transfer(parse6decimal('7500'), userA, ethMarket) - await transfer(parse6decimal('7500'), userA, btcMarket) - - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(btcMarket.address, accountA.address, utils.parseEther('2250')) - .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, ethMarket.address, utils.parseEther('2250')) - .to.emit(controller, 'GroupRebalanced') - .withArgs(userA.address, 1) - - // ensure group collateral unchanged and cannot rebalance - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(parse6decimal('15000')) - expect(canRebalance).to.be.false - }) - it('handles groups with no collateral', async () => { - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(0) - expect(canRebalance).to.be.false + // ensures user has expected amount of collateral in a market + async function expectMarketCollateralBalance(user: SignerWithAddress, amount: BigNumber) { + const local = await ethMarket.locals(user.address) + expect(local.collateral).to.equal(amount) + } - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)).to.be.revertedWithCustomError( - controller, - 'ControllerGroupBalancedError', - ) - }) + // funds specified wallet with 50k collateral + async function fundWallet(wallet: SignerWithAddress): Promise { + await deployment.fundWalletDSU(wallet, utils.parseEther('50000')) + } - it('rebalances markets with no collateral', async () => { - // transfer funds to one of the markets - await transfer(parse6decimal('15000'), userA, btcMarket) + // create a serial nonce for testing purposes; real users may choose a nonce however they please + function nextNonce(): BigNumber { + lastNonce += 1 + return BigNumber.from(lastNonce) + } - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(btcMarket.address, accountA.address, utils.parseEther('9750')) - .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, ethMarket.address, utils.parseEther('9750')) - .to.emit(controller, 'GroupRebalanced') - .withArgs(userA.address, 1) - - // ensure group collateral unchanged and cannot rebalance - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(parse6decimal('15000')) - expect(canRebalance).to.be.false - }) + // updates the market and returns the version timestamp + async function changePosition( + user: SignerWithAddress, + newMaker = constants.MaxUint256, + newLong = constants.MaxUint256, + newShort = constants.MaxUint256, + ): Promise { + const tx = await ethMarket + .connect(user) + ['update(address,uint256,uint256,uint256,int256,bool)']( + user.address, + newMaker, + newLong, + newShort, + 0, + false, + TX_OVERRIDES, + ) + return (await getEventArguments(tx, 'OrderCreated')).order.timestamp + } - it('rebalances markets with no collateral when others are within threshold', async () => { - // reconfigure group such that ETH market has threshold higher than it's imbalance - const message = { - group: 1, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('0.9'), threshold: parse6decimal('0.15') }, - { target: parse6decimal('0.1'), threshold: parse6decimal('0.03') }, - ], - maxFee: constants.Zero, - ...(await createAction(userA.address)), + // performs a market transfer, returning the timestamp of the order produced + async function transfer( + amount: BigNumber, + user: SignerWithAddress, + market = ethMarket, + signer = user, + ): Promise { + const marketTransferMessage = { + market: market.address, + amount: amount, + ...createAction(user.address, signer.address), } - const signature = await signRebalanceConfigChange(userA, verifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be.reverted - - // transfer funds only to the ETH market - await transfer(parse6decimal('10000'), userA, ethMarket) - - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(ethMarket.address, accountA.address, utils.parseEther('1000')) - .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, btcMarket.address, utils.parseEther('1000')) - .to.emit(controller, 'GroupRebalanced') - .withArgs(userA.address, 1) - - // ensure group collateral unchanged and cannot rebalance - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(parse6decimal('10000')) - expect(canRebalance).to.be.false - }) - - it('should not rebalance empty market configured to be empty', async () => { - // reconfigure group such that BTC market is empty - const message = { - group: 1, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('1'), threshold: parse6decimal('0.05') }, - { target: parse6decimal('0'), threshold: parse6decimal('0.05') }, - ], - maxFee: constants.Zero, - ...(await createAction(userA.address)), + const signature = await signMarketTransfer(signer, verifier, marketTransferMessage) + + // determine expected event parameters + let expectedFrom: Address, expectedTo: Address, expectedAmount: BigNumber + if (amount.gt(constants.Zero)) { + // deposits transfer from collateral account into market + expectedFrom = accountA.address + expectedTo = market.address + if (amount === constants.MaxInt256) expectedAmount = await dsu.balanceOf(accountA.address) + else expectedAmount = amount.mul(1e12) + } else { + // withdrawals transfer from market into account + expectedFrom = market.address + expectedTo = accountA.address + if (amount === constants.MinInt256) expectedAmount = (await market.locals(user.address)).collateral.mul(1e12) + else expectedAmount = amount.mul(-1e12) } - const signature = await signRebalanceConfigChange(userA, verifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be.reverted - - // transfer funds to the ETH market - await transfer(parse6decimal('2500'), userA, ethMarket) - // ensure group balanced - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)).to.be.revertedWithCustomError( - controller, - 'ControllerGroupBalancedError', + // perform transfer + await expect( + await controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), ) - }) - - it('should rebalance non-empty market configured to be empty', async () => { - // reconfigure group such that BTC market is empty - const message = { - group: 1, - markets: [ethMarket.address, btcMarket.address], - configs: [ - { target: parse6decimal('1'), threshold: parse6decimal('0.05') }, - { target: parse6decimal('0'), threshold: parse6decimal('0.05') }, - ], - maxFee: constants.Zero, - ...(await createAction(userA.address)), - } - const signature = await signRebalanceConfigChange(userA, verifier, message) - await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be.reverted - - // transfer funds to both markets - await transfer(parse6decimal('2500'), userA, ethMarket) - await transfer(parse6decimal('2500'), userA, btcMarket) - - await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) - .to.emit(dsu, 'Transfer') - .withArgs(btcMarket.address, accountA.address, utils.parseEther('2500')) .to.emit(dsu, 'Transfer') - .withArgs(accountA.address, ethMarket.address, utils.parseEther('2500')) - .to.emit(controller, 'GroupRebalanced') - .withArgs(userA.address, 1) - - // ensure group collateral unchanged and cannot rebalance - const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) - expect(groupCollateral).to.equal(parse6decimal('5000')) - expect(canRebalance).to.be.false - }) - }) - - describe('#transfer', () => { - it('can deposit funds to a market', async () => { - // sign a message to deposit 6k from the collateral account to the market - const transferAmount = parse6decimal('6000') - await transfer(transferAmount, userA) - - // verify balances - await expectMarketCollateralBalance(userA, transferAmount) - expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('9000')) // 15k-6k - }) - - it('implicitly unwraps funds to deposit to a market', async () => { - // account starts with 15k DSU - expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('15000')) - // deposit 5k USDC into the account - const depositAmount = parse6decimal('5000') - await fundWalletUSDC(userA, depositAmount) - await usdc.connect(userA).transfer(accountA.address, depositAmount) - expect(await usdc.balanceOf(accountA.address)).to.equal(depositAmount) - - // deposit all 20k into the market - const transferAmount = parse6decimal('20000') - await transfer(transferAmount, userA) - - // verify balances - await expectMarketCollateralBalance(userA, parse6decimal('20000')) - expect(await dsu.balanceOf(accountA.address)).to.equal(0) - expect(await usdc.balanceOf(accountA.address)).to.equal(0) - }) - - it('delegated signer can transfer funds', async () => { - // configure a delegate - await marketFactory.connect(userA).updateSigner(userB.address, true) - - // sign a message to deposit 4k from the collateral account to the market - const transferAmount = parse6decimal('4000') - await transfer(transferAmount, userA, ethMarket, userB) - - // verify balances - await expectMarketCollateralBalance(userA, transferAmount) - expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('11000')) // 15k-4k - }) - - it('can make multiple deposits to same market', async () => { - for (let i = 0; i < 8; ++i) { - currentTime = await transfer(parse6decimal('100'), userA) - await advanceAndSettle(userA, receiver, currentTime) - } - await expectMarketCollateralBalance(userA, parse6decimal('800')) - }) - - it('can withdraw funds from a market', async () => { - // perform an initial deposit - await transfer(parse6decimal('10000'), userA) - - // withdraw 3k from the the market - const transferAmount = parse6decimal('-3000') - await transfer(transferAmount, userA) - - // verify balances - await expectMarketCollateralBalance(userA, parse6decimal('7000')) // 10k-3k - expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('8000')) // 15k-10k+3k - }) - - it('can fully withdraw from a market', async () => { - // deposit 8k - const depositAmount = parse6decimal('8000') - await transfer(depositAmount, userA) - - // sign a message to fully withdraw from the market - await transfer(constants.MinInt256, userA) - - // verify balances - await expectMarketCollateralBalance(userA, constants.Zero) - expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('15000')) - }) - - it('cannot fully withdraw with position', async () => { - // deposit 7k - const depositAmount = parse6decimal('7000') - await transfer(depositAmount, userA) - - // create a maker position - currentTime = await changePosition(userA, parse6decimal('1.5')) - - await advanceAndSettle(userA, receiver) - expect((await ethMarket.positions(userA.address)).maker).to.equal(parse6decimal('1.5')) + .withArgs(expectedFrom, expectedTo, expectedAmount) + .to.emit(market, 'OrderCreated') + .withArgs( + userA.address, + anyValue, + anyValue, + constants.AddressZero, + constants.AddressZero, + constants.AddressZero, + ) + + const order = await market.pendingOrders(user.address, (await market.global()).currentId) + return order.timestamp + } - // sign a message to fully withdraw from the market - const marketTransferMessage = { - market: ethMarket.address, - amount: constants.MinInt256, - ...createAction(userA.address), - } - const signature = await signMarketTransfer(userA, verifier, marketTransferMessage) + const fixture = async () => { + // set up users + ;[owner, userA, userB, keeper, receiver] = await ethers.getSigners() - // ensure transfer reverts - await expect( - controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ).to.be.revertedWithCustomError(ethMarket, 'MarketInsufficientMarginError') + // deploy protocol + deployment = await deployProtocol(owner, true, true) + marketFactory = deployment.marketFactory + dsu = deployment.dsu + usdc = deployment.usdc + ethMarket = deployment.ethMarket!.market - // 7000 - one settlement fee - expect((await ethMarket.locals(userA.address)).collateral).to.be.within( - parse6decimal('7000').sub(parse6decimal('1')), - parse6decimal('7000'), + // deploy controller + controller = await deployController( + owner, + usdc.address, + dsu.address, + deployment.dsuReserve.address, + marketFactory.address, ) - }) - - it('rejects withdrawal from unauthorized signer', async () => { - // deposit 6k - await transfer(parse6decimal('6000'), userA) - - // unauthorized user signs transfer message - expect(await marketFactory.signers(accountA.address, userB.address)).to.be.false - const marketTransferMessage = { - market: ethMarket.address, - amount: constants.MinInt256, - ...createAction(userA.address, userB.address), + verifier = await new AccountVerifier__factory(owner).deploy(marketFactory.address) + await controller.initialize(verifier.address) + + // set initial price + await advanceToPrice( + deployment.ethMarket!.keeperOracle, + receiver, + currentTime, + parse6decimal('3116.734999'), + TX_OVERRIDES, + ) + lastPrice = (await deployment.ethMarket!.oracle.status())[0].price + + // create a collateral account for userA with 15k collateral in it + await fundWallet(userA) + const accountAddressA = await controller.getAccountAddress(userA.address) + await dsu.connect(userA).transfer(accountAddressA, utils.parseEther('15000')) + currentTime = BigNumber.from(await currentBlockTimestamp()) + const deployAccountMessage = { + ...createAction(userA.address), } - const signature = await signMarketTransfer(userB, verifier, marketTransferMessage) + const signature = await signDeployAccount(userA, verifier, deployAccountMessage) + await controller.connect(keeper).deployAccountWithSignature(deployAccountMessage, signature) + accountA = Account__factory.connect(accountAddressA, userA) - // ensure withdrawal fails - await expect( - controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), - ).to.be.revertedWithCustomError(verifier, 'VerifierInvalidSignerError') - }) - }) + // approve the collateral account as operator + await marketFactory.connect(userA).updateOperator(accountA.address, true) + } - describe('#withdrawal', () => { - let usdcBalanceBefore: BigNumber beforeEach(async () => { - usdcBalanceBefore = await usdc.balanceOf(userA.address) + currentTime = BigNumber.from(await currentBlockTimestamp()) + await loadFixture(fixture) }) - it('can unwrap and partially withdraw funds from a signed message', async () => { - // sign message to perform a partial withdrawal - const withdrawalAmount = parse6decimal('6000') - const withdrawalMessage = { - amount: withdrawalAmount, - unwrap: true, - ...createAction(userA.address), - } - const signature = await signWithdrawal(userA, verifier, withdrawalMessage) - - // perform withdrawal and check balance - await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature)) - .to.emit(usdc, 'Transfer') - .withArgs(accountA.address, userA.address, withdrawalAmount) - - // ensure owner was credited the USDC and account's DSU was debited - expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.equal(withdrawalAmount) - expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('9000')) // 15k-9k - expect(await usdc.balanceOf(accountA.address)).to.equal(0) // no USDC was deposited + describe('#rebalance', () => { + let btcMarket: IMarket + + beforeEach(async () => { + // create another market, including requisite oracles, and set initial price + btcMarket = deployment.btcMarket!.market + const btcKeeperOracle = deployment.btcMarket!.keeperOracle + await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('60606.369'), TX_OVERRIDES) + + // configure a group with both markets + const message = { + group: 1, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('0.65'), threshold: parse6decimal('0.04') }, + { target: parse6decimal('0.35'), threshold: parse6decimal('0.03') }, + ], + maxFee: constants.Zero, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, verifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be + .reverted + }) + + it('checks a group within targets', async () => { + // transfer funds to the markets + await transfer(parse6decimal('9700'), userA, ethMarket) + await transfer(parse6decimal('5000'), userA, btcMarket) + + // check the group + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(parse6decimal('14700')) + expect(canRebalance).to.be.false + }) + + it('checks a group outside of targets', async () => { + // transfer funds to the markets + await transfer(parse6decimal('5000'), userA, ethMarket) + await transfer(parse6decimal('5000'), userA, btcMarket) + + // check the group + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(parse6decimal('10000')) + expect(canRebalance).to.be.true + }) + + it('should not rebalance an already-balanced group', async () => { + // transfer funds to the markets + await transfer(parse6decimal('9700'), userA, ethMarket) + await transfer(parse6decimal('5000'), userA, btcMarket) + + // attempt rebalance + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)).to.be.revertedWithCustomError( + controller, + 'ControllerGroupBalancedError', + ) + }) + + it('rebalances group outside of threshold', async () => { + // transfer funds to the markets + await transfer(parse6decimal('7500'), userA, ethMarket) + await transfer(parse6decimal('7500'), userA, btcMarket) + + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) + .to.emit(dsu, 'Transfer') + .withArgs(btcMarket.address, accountA.address, utils.parseEther('2250')) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, ethMarket.address, utils.parseEther('2250')) + .to.emit(controller, 'GroupRebalanced') + .withArgs(userA.address, 1) + + // ensure group collateral unchanged and cannot rebalance + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(parse6decimal('15000')) + expect(canRebalance).to.be.false + }) + + it('handles groups with no collateral', async () => { + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(0) + expect(canRebalance).to.be.false + + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)).to.be.revertedWithCustomError( + controller, + 'ControllerGroupBalancedError', + ) + }) + + it('rebalances markets with no collateral', async () => { + // transfer funds to one of the markets + await transfer(parse6decimal('15000'), userA, btcMarket) + + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) + .to.emit(dsu, 'Transfer') + .withArgs(btcMarket.address, accountA.address, utils.parseEther('9750')) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, ethMarket.address, utils.parseEther('9750')) + .to.emit(controller, 'GroupRebalanced') + .withArgs(userA.address, 1) + + // ensure group collateral unchanged and cannot rebalance + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(parse6decimal('15000')) + expect(canRebalance).to.be.false + }) + + it('rebalances markets with no collateral when others are within threshold', async () => { + // reconfigure group such that ETH market has threshold higher than it's imbalance + const message = { + group: 1, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('0.9'), threshold: parse6decimal('0.15') }, + { target: parse6decimal('0.1'), threshold: parse6decimal('0.03') }, + ], + maxFee: constants.Zero, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, verifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be + .reverted + + // transfer funds only to the ETH market + await transfer(parse6decimal('10000'), userA, ethMarket) + + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) + .to.emit(dsu, 'Transfer') + .withArgs(ethMarket.address, accountA.address, utils.parseEther('1000')) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, btcMarket.address, utils.parseEther('1000')) + .to.emit(controller, 'GroupRebalanced') + .withArgs(userA.address, 1) + + // ensure group collateral unchanged and cannot rebalance + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(parse6decimal('10000')) + expect(canRebalance).to.be.false + }) + + it('should not rebalance empty market configured to be empty', async () => { + // reconfigure group such that BTC market is empty + const message = { + group: 1, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('1'), threshold: parse6decimal('0.05') }, + { target: parse6decimal('0'), threshold: parse6decimal('0.05') }, + ], + maxFee: constants.Zero, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, verifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be + .reverted + + // transfer funds to the ETH market + await transfer(parse6decimal('2500'), userA, ethMarket) + + // ensure group balanced + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)).to.be.revertedWithCustomError( + controller, + 'ControllerGroupBalancedError', + ) + }) + + it('should rebalance non-empty market configured to be empty', async () => { + // reconfigure group such that BTC market is empty + const message = { + group: 1, + markets: [ethMarket.address, btcMarket.address], + configs: [ + { target: parse6decimal('1'), threshold: parse6decimal('0.05') }, + { target: parse6decimal('0'), threshold: parse6decimal('0.05') }, + ], + maxFee: constants.Zero, + ...(await createAction(userA.address)), + } + const signature = await signRebalanceConfigChange(userA, verifier, message) + await expect(controller.connect(keeper).changeRebalanceConfigWithSignature(message, signature)).to.not.be + .reverted + + // transfer funds to both markets + await transfer(parse6decimal('2500'), userA, ethMarket) + await transfer(parse6decimal('2500'), userA, btcMarket) + + await expect(controller.rebalanceGroup(userA.address, 1, TX_OVERRIDES)) + .to.emit(dsu, 'Transfer') + .withArgs(btcMarket.address, accountA.address, utils.parseEther('2500')) + .to.emit(dsu, 'Transfer') + .withArgs(accountA.address, ethMarket.address, utils.parseEther('2500')) + .to.emit(controller, 'GroupRebalanced') + .withArgs(userA.address, 1) + + // ensure group collateral unchanged and cannot rebalance + const [groupCollateral, canRebalance] = await controller.callStatic.checkGroup(userA.address, 1) + expect(groupCollateral).to.equal(parse6decimal('5000')) + expect(canRebalance).to.be.false + }) }) - it('can fully withdraw from a delegated signer', async () => { - // configure userB as delegated signer - await marketFactory.connect(userA).updateSigner(userB.address, true) - - // delegate signs message for full withdrawal - const withdrawalMessage = { - amount: constants.MaxUint256, - unwrap: true, - ...createAction(userA.address, userB.address), - } - const signature = await signWithdrawal(userB, verifier, withdrawalMessage) - - // perform withdrawal and check balance - await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature)).to.not.be.reverted - - // ensure owner was credit all the USDC and account is empty - expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.equal(parse6decimal('15000')) - expect(await dsu.balanceOf(accountA.address)).to.equal(0) // all DSU was withdrawan - expect(await usdc.balanceOf(accountA.address)).to.equal(0) // no USDC was deposited + describe('#transfer', () => { + it('can deposit funds to a market', async () => { + // sign a message to deposit 6k from the collateral account to the market + const transferAmount = parse6decimal('6000') + await transfer(transferAmount, userA) + + // verify balances + await expectMarketCollateralBalance(userA, transferAmount) + expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('9000')) // 15k-6k + }) + + it('implicitly unwraps funds to deposit to a market', async () => { + // account starts with 15k DSU + expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('15000')) + // deposit 5k USDC into the account + const depositAmount = parse6decimal('5000') + await deployment.fundWalletUSDC(userA, depositAmount) + await usdc.connect(userA).transfer(accountA.address, depositAmount) + expect(await usdc.balanceOf(accountA.address)).to.equal(depositAmount) + + // deposit all 20k into the market + const transferAmount = parse6decimal('20000') + await transfer(transferAmount, userA) + + // verify balances + await expectMarketCollateralBalance(userA, parse6decimal('20000')) + expect(await dsu.balanceOf(accountA.address)).to.equal(0) + expect(await usdc.balanceOf(accountA.address)).to.equal(0) + }) + + it('delegated signer can transfer funds', async () => { + // configure a delegate + await marketFactory.connect(userA).updateSigner(userB.address, true) + + // sign a message to deposit 4k from the collateral account to the market + const transferAmount = parse6decimal('4000') + await transfer(transferAmount, userA, ethMarket, userB) + + // verify balances + await expectMarketCollateralBalance(userA, transferAmount) + expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('11000')) // 15k-4k + }) + + it('can make multiple deposits to same market', async () => { + for (let i = 0; i < 8; ++i) { + currentTime = await transfer(parse6decimal('100'), userA) + await advanceAndSettle(userA, receiver, currentTime) + } + await expectMarketCollateralBalance(userA, parse6decimal('800')) + }) + + it('can withdraw funds from a market', async () => { + // perform an initial deposit + await transfer(parse6decimal('10000'), userA) + + // withdraw 3k from the the market + const transferAmount = parse6decimal('-3000') + await transfer(transferAmount, userA) + + // verify balances + await expectMarketCollateralBalance(userA, parse6decimal('7000')) // 10k-3k + expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('8000')) // 15k-10k+3k + }) + + it('can fully withdraw from a market', async () => { + // deposit 8k + const depositAmount = parse6decimal('8000') + await transfer(depositAmount, userA) + + // sign a message to fully withdraw from the market + await transfer(constants.MinInt256, userA) + + // verify balances + await expectMarketCollateralBalance(userA, constants.Zero) + expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('15000')) + }) + + it('cannot fully withdraw with position', async () => { + // deposit 7k + const depositAmount = parse6decimal('7000') + await transfer(depositAmount, userA) + + // create a maker position + currentTime = await changePosition(userA, parse6decimal('1.5')) + + await advanceAndSettle(userA, receiver) + expect((await ethMarket.positions(userA.address)).maker).to.equal(parse6decimal('1.5')) + + // sign a message to fully withdraw from the market + const marketTransferMessage = { + market: ethMarket.address, + amount: constants.MinInt256, + ...createAction(userA.address), + } + const signature = await signMarketTransfer(userA, verifier, marketTransferMessage) + + // ensure transfer reverts + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ).to.be.revertedWithCustomError(ethMarket, 'MarketInsufficientMarginError') + + // 7000 - one settlement fee + expect((await ethMarket.locals(userA.address)).collateral).to.be.within( + parse6decimal('7000').sub(parse6decimal('1')), + parse6decimal('7000'), + ) + }) + + it('rejects withdrawal from unauthorized signer', async () => { + // deposit 6k + await transfer(parse6decimal('6000'), userA) + + // unauthorized user signs transfer message + expect(await marketFactory.signers(accountA.address, userB.address)).to.be.false + const marketTransferMessage = { + market: ethMarket.address, + amount: constants.MinInt256, + ...createAction(userA.address, userB.address), + } + const signature = await signMarketTransfer(userB, verifier, marketTransferMessage) + + // ensure withdrawal fails + await expect( + controller.connect(keeper).marketTransferWithSignature(marketTransferMessage, signature, TX_OVERRIDES), + ).to.be.revertedWithCustomError(verifier, 'VerifierInvalidSignerError') + }) }) - it('rejects withdrawals from unauthorized signer', async () => { - expect(await marketFactory.signers(accountA.address, userB.address)).to.be.false - - // unauthorized user signs message for withdrawal - const withdrawalMessage = { - amount: parse6decimal('2000'), - unwrap: false, - ...createAction(userA.address, userB.address), - } - const signature = await signWithdrawal(userB, verifier, withdrawalMessage) - - // ensure withdrawal fails - await expect( - controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature), - ).to.be.revertedWithCustomError(verifier, 'VerifierInvalidSignerError') + describe('#withdrawal', () => { + let usdcBalanceBefore: BigNumber + beforeEach(async () => { + usdcBalanceBefore = await usdc.balanceOf(userA.address) + }) + + it('can unwrap and partially withdraw funds from a signed message', async () => { + // sign message to perform a partial withdrawal + const withdrawalAmount = parse6decimal('6000') + const withdrawalMessage = { + amount: withdrawalAmount, + unwrap: true, + ...createAction(userA.address), + } + const signature = await signWithdrawal(userA, verifier, withdrawalMessage) + + // perform withdrawal and check balance + await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature)) + .to.emit(usdc, 'Transfer') + .withArgs(accountA.address, userA.address, withdrawalAmount) + + // ensure owner was credited the USDC and account's DSU was debited + expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.equal(withdrawalAmount) + expect(await dsu.balanceOf(accountA.address)).to.equal(utils.parseEther('9000')) // 15k-9k + expect(await usdc.balanceOf(accountA.address)).to.equal(0) // no USDC was deposited + }) + + it('can fully withdraw from a delegated signer', async () => { + // configure userB as delegated signer + await marketFactory.connect(userA).updateSigner(userB.address, true) + + // delegate signs message for full withdrawal + const withdrawalMessage = { + amount: constants.MaxUint256, + unwrap: true, + ...createAction(userA.address, userB.address), + } + const signature = await signWithdrawal(userB, verifier, withdrawalMessage) + + // perform withdrawal and check balance + await expect(controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature)).to.not.be.reverted + + // ensure owner was credit all the USDC and account is empty + expect((await usdc.balanceOf(userA.address)).sub(usdcBalanceBefore)).to.equal(parse6decimal('15000')) + expect(await dsu.balanceOf(accountA.address)).to.equal(0) // all DSU was withdrawan + expect(await usdc.balanceOf(accountA.address)).to.equal(0) // no USDC was deposited + }) + + it('rejects withdrawals from unauthorized signer', async () => { + expect(await marketFactory.signers(accountA.address, userB.address)).to.be.false + + // unauthorized user signs message for withdrawal + const withdrawalMessage = { + amount: parse6decimal('2000'), + unwrap: false, + ...createAction(userA.address, userB.address), + } + const signature = await signWithdrawal(userB, verifier, withdrawalMessage) + + // ensure withdrawal fails + await expect( + controller.connect(keeper).withdrawWithSignature(withdrawalMessage, signature), + ).to.be.revertedWithCustomError(verifier, 'VerifierInvalidSignerError') + }) }) }) -}) +} diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index 6b20a6982..7e74e91c4 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -51,7 +51,12 @@ const TX_OVERRIDES = { gasLimit: 3_000_000, maxPriorityFeePerGas: 0, maxFeePerGa export function RunIncentivizedTests( name: string, - deployProtocol: (owner: SignerWithAddress, overrides?: CallOverrides) => Promise, + deployProtocol: ( + owner: SignerWithAddress, + createMarketETH: boolean, + createMarketBTC: boolean, + overrides?: CallOverrides, + ) => Promise, deployInstance: ( owner: SignerWithAddress, marketFactory: IMarketFactory, @@ -69,8 +74,6 @@ export function RunIncentivizedTests( let marketFactory: IMarketFactory let ethMarket: IMarket let btcMarket: IMarket - let ethKeeperOracle: IKeeperOracle - let btcKeeperOracle: IKeeperOracle let owner: SignerWithAddress let userA: SignerWithAddress let userB: SignerWithAddress @@ -179,15 +182,12 @@ export function RunIncentivizedTests( const fixture = async () => { // deploy the protocol ;[owner, userA, userB, userC, keeper, receiver] = await ethers.getSigners() - deployment = await deployProtocol(owner, TX_OVERRIDES) - // TODO: eliminate infrequently used member variables + deployment = await deployProtocol(owner, true, true, TX_OVERRIDES) dsu = deployment.dsu usdc = deployment.usdc marketFactory = deployment.marketFactory - ethMarket = deployment.ethMarket - btcMarket = deployment.btcMarket - ethKeeperOracle = deployment.ethKeeperOracle - btcKeeperOracle = deployment.btcKeeperOracle + ethMarket = deployment.ethMarket!.market + btcMarket = deployment.btcMarket!.market ;[controller, accountVerifier] = await deployInstance( owner, deployment.marketFactory, @@ -195,8 +195,20 @@ export function RunIncentivizedTests( TX_OVERRIDES, ) - await advanceToPrice(ethKeeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES) - await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('57575.464'), TX_OVERRIDES) + await advanceToPrice( + deployment.ethMarket!.keeperOracle, + receiver, + currentTime, + parse6decimal('3113.7128'), + TX_OVERRIDES, + ) + await advanceToPrice( + deployment.btcMarket!.keeperOracle, + receiver, + currentTime, + parse6decimal('57575.464'), + TX_OVERRIDES, + ) // fund userA await dsu.connect(userA).approve(ethMarket.address, constants.MaxUint256, { maxFeePerGas: 100000000 }) diff --git a/packages/perennial-account/test/integration/Optimism.test.ts b/packages/perennial-account/test/integration/Optimism.test.ts index 02dbc7cd0..09cace943 100644 --- a/packages/perennial-account/test/integration/Optimism.test.ts +++ b/packages/perennial-account/test/integration/Optimism.test.ts @@ -2,54 +2,60 @@ import HRE from 'hardhat' import { smock } from '@defi-wonderland/smock' import { CallOverrides } from 'ethers' -import { AccountVerifier__factory, AggregatorV3Interface, OptGasInfo } from '../../types/generated' +import { AccountVerifier__factory, AggregatorV3Interface, IAccountVerifier, OptGasInfo } from '../../types/generated' import { createFactoriesForChain, deployControllerOptimism, fundWalletDSU, fundWalletUSDC, + getDSUReserve, getStablecoins, } from '../helpers/baseHelpers' -import { createMarketBTC, createMarketETH, DeploymentVars } from '../helpers/setupHelpers' +import { + createMarketBTC as setupMarketBTC, + createMarketETH as setupMarketETH, + DeploymentVars, +} from '../helpers/setupHelpers' import { RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory } from '../../types/generated' import { RunAccountTests } from './Account.test' +import { RunControllerBaseTests } from './Controller.test' const { ethers } = HRE -async function deployProtocol(owner: SignerWithAddress, overrides?: CallOverrides): Promise { +async function deployProtocol( + owner: SignerWithAddress, + createMarketETH = false, + createMarketBTC = false, + overrides?: CallOverrides, +): Promise { const [oracleFactory, marketFactory, pythOracleFactory, chainlinkKeptFeed] = await createFactoriesForChain(owner) const [dsu, usdc] = await getStablecoins(owner) - const [ethMarket, , ethKeeperOracle] = await createMarketETH( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - ) - const [btcMarket, , btcKeeperOracle] = await createMarketBTC( - owner, - oracleFactory, - pythOracleFactory, - marketFactory, - dsu, - overrides, - ) - return { + + const deployment: DeploymentVars = { dsu, usdc, oracleFactory, pythOracleFactory, marketFactory, - ethMarket, - btcMarket, - ethKeeperOracle, - btcKeeperOracle, + ethMarket: undefined, // TODO: style: inlining these was difficult to read; set below + btcMarket: undefined, chainlinkKeptFeed, + dsuReserve: getDSUReserve(owner), fundWalletDSU, fundWalletUSDC, } + + if (createMarketETH) { + deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + } + + if (createMarketBTC) { + deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + } + + return deployment } async function deployInstance( @@ -57,7 +63,7 @@ async function deployInstance( marketFactory: IMarketFactory, chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, -): Promise { +): Promise<[Controller_Incentivized, IAccountVerifier]> { // FIXME: erroring with "trying to deploy a contract whose code is too large" when I pass empty overrides const controller = await deployControllerOptimism(owner, marketFactory /*, overrides ?? {}*/) @@ -107,6 +113,8 @@ async function mockGasInfo() { } if (process.env.FORK_NETWORK === 'base') { + // TODO: Would it be faster to deploy the protocol once with both markets, and let each test suite take their own snapshots? RunAccountTests(deployProtocol, deployInstance) + RunControllerBaseTests(deployProtocol) RunIncentivizedTests('Controller_Optimism', deployProtocol, deployInstance, mockGasInfo) } From 6adb68a5fd217e6b19ef7eb2480850863af1366a Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Thu, 17 Oct 2024 15:25:44 -0400 Subject: [PATCH 10/23] resolved issue with gas limit --- packages/common/hardhat.default.config.ts | 2 +- .../perennial-account/test/integration/Arbitrum.test.ts | 7 +++---- .../test/integration/Controller_Incentivized.test.ts | 5 +++-- .../perennial-account/test/integration/Optimism.test.ts | 7 +++---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/common/hardhat.default.config.ts b/packages/common/hardhat.default.config.ts index 6dad7d809..79db6ef35 100644 --- a/packages/common/hardhat.default.config.ts +++ b/packages/common/hardhat.default.config.ts @@ -126,7 +126,7 @@ export default function defaultConfig({ : undefined, chainId: getChainId('hardhat'), allowUnlimitedContractSize: true, - blockGasLimit: 32000000, + blockGasLimit: 32_000_000, mining: NODE_INTERVAL_MINING ? { interval: NODE_INTERVAL_MINING, diff --git a/packages/perennial-account/test/integration/Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts index 5bd146c30..94cf5664c 100644 --- a/packages/perennial-account/test/integration/Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -54,11 +54,11 @@ async function deployProtocol( } if (createMarketETH) { - deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) } if (createMarketBTC) { - deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) } return deployment @@ -70,8 +70,7 @@ async function deployInstance( chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, ): Promise<[Controller_Incentivized, IAccountVerifier]> { - // FIXME: erroring with "trying to deploy a contract whose code is too large" when I pass empty overrides - const controller = await deployControllerArbitrum(owner, marketFactory /*, overrides ?? {}*/) + const controller = await deployControllerArbitrum(owner, marketFactory, overrides ?? {}) const keepConfig = { multiplierBase: ethers.utils.parseEther('1'), diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index 7e74e91c4..9374083df 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -40,14 +40,15 @@ import { signSignerUpdate, } from '@equilibria/perennial-v2-verifier/test/helpers/erc712' import { Verifier, Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' -import { AggregatorV3Interface, IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' +import { AggregatorV3Interface } from '@equilibria/perennial-v2-oracle/types/generated' const { ethers } = HRE const DEFAULT_MAX_FEE = parse6decimal('0.5') // hack around issues estimating gas for instrumented contracts when running tests under coverage -const TX_OVERRIDES = { gasLimit: 3_000_000, maxPriorityFeePerGas: 0, maxFeePerGas: 100_000_000 } +// also, need higher gasLimit to deploy incentivized controllers with optimizer disabled +const TX_OVERRIDES = { gasLimit: 12_000_000, maxPriorityFeePerGas: 0, maxFeePerGas: 100_000_000 } export function RunIncentivizedTests( name: string, diff --git a/packages/perennial-account/test/integration/Optimism.test.ts b/packages/perennial-account/test/integration/Optimism.test.ts index 09cace943..e73ec510d 100644 --- a/packages/perennial-account/test/integration/Optimism.test.ts +++ b/packages/perennial-account/test/integration/Optimism.test.ts @@ -48,11 +48,11 @@ async function deployProtocol( } if (createMarketETH) { - deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) } if (createMarketBTC) { - deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) + deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) } return deployment @@ -64,8 +64,7 @@ async function deployInstance( chainlinkKeptFeed: AggregatorV3Interface, overrides?: CallOverrides, ): Promise<[Controller_Incentivized, IAccountVerifier]> { - // FIXME: erroring with "trying to deploy a contract whose code is too large" when I pass empty overrides - const controller = await deployControllerOptimism(owner, marketFactory /*, overrides ?? {}*/) + const controller = await deployControllerOptimism(owner, marketFactory, overrides ?? {}) const keepConfig = { multiplierBase: ethers.utils.parseEther('1'), From cd4692e490f48eb035421ce9182f54666e184303 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Thu, 17 Oct 2024 15:51:08 -0400 Subject: [PATCH 11/23] reverted unintentional chicken --- .../test/helpers/arbitrumHelpers.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/perennial-order/test/helpers/arbitrumHelpers.ts b/packages/perennial-order/test/helpers/arbitrumHelpers.ts index d965ae36f..8742cba5c 100644 --- a/packages/perennial-order/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-order/test/helpers/arbitrumHelpers.ts @@ -2,7 +2,9 @@ import { expect } from 'chai' import { BigNumber, CallOverrides, constants, utils } from 'ethers' import { IMarket, MarketFactory, MarketFactory__factory } from '@equilibria/perennial-v2/types/generated' import { + IKeeperOracle, IOracleFactory, + IOracleProvider, KeeperOracle__factory, OracleFactory, PythFactory, @@ -15,14 +17,40 @@ import { IERC20Metadata, IERC20Metadata__factory, IMarketFactory, IVerifier } fr import { impersonate } from '../../../common/testutil' import { Address } from 'hardhat-deploy/dist/types' import { parse6decimal } from '../../../common/testutil/types' -import { deployOracleFactory } from './oracleHelpers' -import { deployMarketImplementation } from './marketHelpers' +import { createPythOracle, deployOracleFactory } from './oracleHelpers' +import { createMarket, deployMarketImplementation } from './marketHelpers' const DSU_ADDRESS = '0x52C64b8998eB7C80b6F526E99E29ABdcC86B841b' // Digital Standard Unit, an 18-decimal token const DSU_HOLDER = '0x90a664846960aafa2c164605aebb8e9ac338f9a0' // Perennial Market has 4.7mm at height 243648015 const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' +const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' +// creates an ETH market using a locally deployed factory and oracle +export async function createMarketETH( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythOracleFactory: PythFactory, + marketFactory: IMarketFactory, + dsu: IERC20Metadata, + overrides?: CallOverrides, +): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { + // Create oracles needed to support the market + const [keeperOracle, oracle] = await createPythOracle( + owner, + oracleFactory, + pythOracleFactory, + PYTH_ETH_USD_PRICE_FEED, + 'ETH-USD', + overrides, + ) + // Create the market in which user or collateral account may interact + const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) + await keeperOracle.register(oracle.address) + await oracle.register(market.address) + return [market, oracle, keeperOracle] +} + // Deploys the market factory and configures default protocol parameters async function deployMarketFactory( owner: SignerWithAddress, From 9ddd4734bc16e98e976dca446e179e535bb46047 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 12:05:30 -0400 Subject: [PATCH 12/23] refactored arbitrum-specific stuff out of manager integration test --- .../test/helpers/arbitrumHelpers.ts | 167 --- .../test/helpers/setupHelpers.ts | 31 + .../test/integration/Manager.test.ts | 975 +++++++++++++ .../test/integration/Manager_Arbitrum.test.ts | 1250 ++++------------- 4 files changed, 1264 insertions(+), 1159 deletions(-) delete mode 100644 packages/perennial-order/test/helpers/arbitrumHelpers.ts create mode 100644 packages/perennial-order/test/helpers/setupHelpers.ts create mode 100644 packages/perennial-order/test/integration/Manager.test.ts diff --git a/packages/perennial-order/test/helpers/arbitrumHelpers.ts b/packages/perennial-order/test/helpers/arbitrumHelpers.ts deleted file mode 100644 index 8742cba5c..000000000 --- a/packages/perennial-order/test/helpers/arbitrumHelpers.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { expect } from 'chai' -import { BigNumber, CallOverrides, constants, utils } from 'ethers' -import { IMarket, MarketFactory, MarketFactory__factory } from '@equilibria/perennial-v2/types/generated' -import { - IKeeperOracle, - IOracleFactory, - IOracleProvider, - KeeperOracle__factory, - OracleFactory, - PythFactory, - PythFactory__factory, - GasOracle__factory, -} from '@equilibria/perennial-v2-oracle/types/generated' -import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { IERC20Metadata, IERC20Metadata__factory, IMarketFactory, IVerifier } from '../../types/generated' -import { impersonate } from '../../../common/testutil' -import { Address } from 'hardhat-deploy/dist/types' -import { parse6decimal } from '../../../common/testutil/types' -import { createPythOracle, deployOracleFactory } from './oracleHelpers' -import { createMarket, deployMarketImplementation } from './marketHelpers' - -const DSU_ADDRESS = '0x52C64b8998eB7C80b6F526E99E29ABdcC86B841b' // Digital Standard Unit, an 18-decimal token -const DSU_HOLDER = '0x90a664846960aafa2c164605aebb8e9ac338f9a0' // Perennial Market has 4.7mm at height 243648015 -const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' -const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' -const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' - -// creates an ETH market using a locally deployed factory and oracle -export async function createMarketETH( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, - pythOracleFactory: PythFactory, - marketFactory: IMarketFactory, - dsu: IERC20Metadata, - overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { - // Create oracles needed to support the market - const [keeperOracle, oracle] = await createPythOracle( - owner, - oracleFactory, - pythOracleFactory, - PYTH_ETH_USD_PRICE_FEED, - 'ETH-USD', - overrides, - ) - // Create the market in which user or collateral account may interact - const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) - await keeperOracle.register(oracle.address) - await oracle.register(market.address) - return [market, oracle, keeperOracle] -} - -// Deploys the market factory and configures default protocol parameters -async function deployMarketFactory( - owner: SignerWithAddress, - pauser: SignerWithAddress, - oracleFactoryAddress: Address, - verifierAddress: Address, - marketImplAddress: Address, -): Promise { - const marketFactory = await new MarketFactory__factory(owner).deploy( - oracleFactoryAddress, - verifierAddress, - marketImplAddress, - ) - await marketFactory.connect(owner).initialize() - - // Set protocol parameters - await marketFactory.updatePauser(pauser.address) - await marketFactory.updateParameter({ - maxFee: parse6decimal('0.01'), - maxLiquidationFee: parse6decimal('20'), - maxCut: parse6decimal('0.50'), - maxRate: parse6decimal('10.00'), - minMaintenance: parse6decimal('0.01'), - minEfficiency: parse6decimal('0.1'), - referralFee: 0, - minScale: parse6decimal('0.001'), - maxStaleAfter: 7200, - }) - - return marketFactory -} - -// Deploys OracleFactory and then MarketFactory -export async function deployProtocol( - owner: SignerWithAddress, -): Promise<[IMarketFactory, IERC20Metadata, IOracleFactory]> { - // Deploy the oracle factory, which markets created by the market factory will query - const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) - const oracleFactory = await deployOracleFactory(owner) - - // Deploy the market factory and authorize it with the oracle factory - const marketVerifier = await new Verifier__factory(owner).deploy() - const marketFactory = await deployProtocolForOracle(owner, oracleFactory, marketVerifier) - return [marketFactory, dsu, oracleFactory] -} - -// Deploys a market implementation and the MarketFactory for a provided oracle factory -async function deployProtocolForOracle( - owner: SignerWithAddress, - oracleFactory: OracleFactory, - verifier: IVerifier, -): Promise { - // Deploy protocol contracts - const marketImpl = await deployMarketImplementation(owner, verifier.address) - const marketFactory = await deployMarketFactory( - owner, - owner, - oracleFactory.address, - verifier.address, - marketImpl.address, - ) - return marketFactory -} - -export async function deployPythOracleFactory( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, -): Promise { - const commitmentGasOracle = await new GasOracle__factory(owner).deploy( - CHAINLINK_ETH_USD_FEED, - 8, - 1_000_000, - utils.parseEther('1.02'), - 1_000_000, - 0, - 0, - 0, - ) - const settlementGasOracle = await new GasOracle__factory(owner).deploy( - CHAINLINK_ETH_USD_FEED, - 8, - 200_000, - utils.parseEther('1.02'), - 500_000, - 0, - 0, - 0, - ) - - // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices - const keeperOracleImpl = await new KeeperOracle__factory(owner).deploy(60) - const pythOracleFactory = await new PythFactory__factory(owner).deploy( - PYTH_ADDRESS, - commitmentGasOracle.address, - settlementGasOracle.address, - keeperOracleImpl.address, - ) - await pythOracleFactory.initialize(oracleFactory.address) - await pythOracleFactory.updateParameter(1, 0, 4, 10) - await oracleFactory.register(pythOracleFactory.address) - return pythOracleFactory -} - -export async function fundWalletDSU( - wallet: SignerWithAddress, - amount: BigNumber, - overrides?: CallOverrides, -): Promise { - const dsuOwner = await impersonate.impersonateWithBalance(DSU_HOLDER, utils.parseEther('10')) - const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, dsuOwner) - - expect(await dsu.balanceOf(DSU_HOLDER)).to.be.greaterThan(amount) - await dsu.transfer(wallet.address, amount, overrides ?? {}) -} diff --git a/packages/perennial-order/test/helpers/setupHelpers.ts b/packages/perennial-order/test/helpers/setupHelpers.ts new file mode 100644 index 000000000..48d5e2844 --- /dev/null +++ b/packages/perennial-order/test/helpers/setupHelpers.ts @@ -0,0 +1,31 @@ +import { BigNumber, CallOverrides } from 'ethers' +import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' +import { + IEmptySetReserve, + IERC20Metadata, + IMarket, + IMarketFactory, + IOracleProvider, + IOrderVerifier, + Manager_Arbitrum, +} from '../../types/generated' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +export interface FixtureVars { + dsu: IERC20Metadata + usdc: IERC20Metadata + reserve: IEmptySetReserve + keeperOracle: IKeeperOracle + manager: Manager_Arbitrum + marketFactory: IMarketFactory + market: IMarket + oracle: IOracleProvider + verifier: IOrderVerifier + owner: SignerWithAddress + userA: SignerWithAddress + userB: SignerWithAddress + userC: SignerWithAddress + userD: SignerWithAddress + keeper: SignerWithAddress + oracleFeeReceiver: SignerWithAddress +} diff --git a/packages/perennial-order/test/integration/Manager.test.ts b/packages/perennial-order/test/integration/Manager.test.ts new file mode 100644 index 000000000..0f8d283f2 --- /dev/null +++ b/packages/perennial-order/test/integration/Manager.test.ts @@ -0,0 +1,975 @@ +import { expect } from 'chai' +import { BigNumber, BigNumberish, CallOverrides, constants, utils } from 'ethers' +import HRE from 'hardhat' +import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +import { advanceBlock, currentBlockTimestamp, increase } from '../../../common/testutil/time' +import { getEventArguments, getTimestamp } from '../../../common/testutil/transaction' +import { parse6decimal } from '../../../common/testutil/types' + +import { IERC20Metadata, IMarketFactory, IMarket, IOracleProvider } from '@equilibria/perennial-v2/types/generated' +import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' +import { IEmptySetReserve, IOrderVerifier, Manager_Arbitrum } from '../../types/generated' + +import { signAction, signCancelOrderAction, signPlaceOrderAction } from '../helpers/eip712' +import { + Compare, + compareOrders, + DEFAULT_TRIGGER_ORDER, + MAGIC_VALUE_CLOSE_POSITION, + orderFromStructOutput, + Side, +} from '../helpers/order' +import { advanceToPrice } from '../helpers/oracleHelpers' +import { PlaceOrderActionStruct } from '../../types/generated/contracts/Manager' +import { Address } from 'hardhat-deploy/dist/types' +import { impersonate } from '../../../common/testutil' +import { FixtureVars } from '../helpers/setupHelpers' + +const MAX_FEE = utils.parseEther('0.88') + +const NO_INTERFACE_FEE = { + interfaceFee: { + amount: constants.Zero, + receiver: constants.AddressZero, + fixedFee: false, + unwrap: false, + }, +} + +// because we called hardhat_setNextBlockBaseFeePerGas, need this when running tests under coverage +const TX_OVERRIDES = { maxPriorityFeePerGas: 0, maxFeePerGas: 150_000_000 } + +export function RunManagerTests( + name: string, + getFixture: (overrides?: CallOverrides) => Promise, + mockGasInfo: () => Promise, +): void { + describe(name, () => { + let dsu: IERC20Metadata + let usdc: IERC20Metadata + let reserve: IEmptySetReserve + let keeperOracle: IKeeperOracle + let manager: Manager_Arbitrum + let marketFactory: IMarketFactory + let market: IMarket + let oracle: IOracleProvider + let verifier: IOrderVerifier + let owner: SignerWithAddress + let userA: SignerWithAddress + let userB: SignerWithAddress + let userC: SignerWithAddress + let userD: SignerWithAddress + let keeper: SignerWithAddress + let oracleFeeReceiver: SignerWithAddress + let currentTime: BigNumber + let keeperBalanceBefore: BigNumber + let keeperEthBalanceBefore: BigNumber + let lastMessageNonce = 0 + let lastPriceCommitted: BigNumber + const nextOrderId: { [key: string]: BigNumber } = {} + + function advanceOrderId(user: SignerWithAddress) { + nextOrderId[user.address] = nextOrderId[user.address].add(BigNumber.from(1)) + } + + async function checkCompensation(priceCommitments = 0) { + await expect(manager.connect(keeper).claim(keeper.address, false, TX_OVERRIDES)) + const keeperFeesPaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) + let keeperEthSpentOnGas = keeperEthBalanceBefore.sub(await keeper.getBalance()) + + // If TXes in test required outside price commitments, compensate the keeper for them. + // Note that calls to `commitPrice` in this module do not consume keeper gas. + keeperEthSpentOnGas = keeperEthSpentOnGas.add(utils.parseEther('0.0000644306').mul(priceCommitments)) + + // cost of transaction + const keeperGasCostInUSD = keeperEthSpentOnGas.mul(2603) + // keeper should be compensated between 100-200% of actual gas cost + expect(keeperFeesPaid).to.be.within(keeperGasCostInUSD, keeperGasCostInUSD.mul(2)) + } + + // commits an oracle version and advances time 10 seconds + async function commitPrice( + price = lastPriceCommitted, + timestamp: BigNumber | undefined = undefined, + ): Promise { + if (!timestamp) timestamp = await oracle.current() + + lastPriceCommitted = price + return advanceToPrice(keeperOracle, oracleFeeReceiver, timestamp!, price, TX_OVERRIDES) + } + + function createActionMessage( + userAddress: Address, + nonce = nextMessageNonce(), + signerAddress = userAddress, + expiresInSeconds = 24, + ) { + return { + action: { + market: market.address, + orderId: nextOrderId[userAddress], + maxFee: MAX_FEE, + common: { + account: userAddress, + signer: signerAddress, + domain: manager.address, + nonce: nonce, + group: 0, + expiry: currentTime.add(expiresInSeconds), + }, + }, + } + } + + async function ensureNoPosition(user: SignerWithAddress) { + const position = await market.positions(user.address) + expect(position.maker).to.equal(0) + expect(position.long).to.equal(0) + expect(position.short).to.equal(0) + const pending = await market.pendings(user.address) + expect(pending.makerPos.sub(pending.makerNeg)).to.equal(0) + expect(pending.longPos.sub(pending.longNeg)).to.equal(0) + expect(pending.shortPos.sub(pending.shortNeg)).to.equal(0) + } + + // executes an order as keeper + async function executeOrder( + user: SignerWithAddress, + orderId: BigNumberish, + expectedInterfaceFee: BigNumber | undefined = undefined, + ): Promise { + // ensure order is executable + const [order, canExecute] = await manager.checkOrder(market.address, user.address, orderId) + expect(canExecute).to.be.true + + // validate event + const tx = await manager.connect(keeper).executeOrder(market.address, user.address, orderId, TX_OVERRIDES) + // set the order's spent flag true to validate event + const spentOrder = { ...orderFromStructOutput(order), isSpent: true } + await expect(tx) + .to.emit(manager, 'TriggerOrderExecuted') + .withArgs(market.address, user.address, spentOrder, orderId) + .to.emit(market, 'OrderCreated') + .withArgs(user.address, anyValue, anyValue, constants.AddressZero, order.referrer, constants.AddressZero) + if (order.interfaceFee.amount.gt(0)) { + if (!expectedInterfaceFee && order.interfaceFee.fixedFee) { + expectedInterfaceFee = order.interfaceFee.amount + } + if (expectedInterfaceFee) { + await expect(tx) + .to.emit(manager, 'TriggerOrderInterfaceFeeCharged') + .withArgs(user.address, market.address, order.interfaceFee) + if (order.interfaceFee.unwrap) { + await expect(tx) + .to.emit(dsu, 'Transfer') + .withArgs(market.address, manager.address, expectedInterfaceFee.mul(1e12)) + } else { + await expect(tx) + .to.emit(dsu, 'Transfer') + .withArgs(market.address, manager.address, expectedInterfaceFee.mul(1e12)) + } + } + } + const timestamp = await getTimestamp(tx) + // ensure trigger order was marked as spent + const deletedOrder = await manager.orders(market.address, user.address, orderId) + expect(deletedOrder.isSpent).to.be.true + + return BigNumber.from(timestamp) + } + + async function getPendingPosition(user: SignerWithAddress, side: Side) { + const position = await market.positions(user.address) + const pending = await market.pendings(user.address) + + let actualPos: BigNumber + let pendingPos: BigNumber + switch (side) { + case Side.MAKER: + actualPos = position.maker + pendingPos = pending.makerPos.sub(pending.makerNeg) + break + case Side.LONG: + actualPos = position.long + pendingPos = pending.longPos.sub(pending.longNeg) + break + case Side.SHORT: + actualPos = position.short + pendingPos = pending.shortPos.sub(pending.shortNeg) + break + default: + throw new Error('Unexpected side') + } + + return actualPos.add(pendingPos) + } + + function nextMessageNonce(): BigNumber { + return BigNumber.from(++lastMessageNonce) + } + + // submits a trigger order, validating event and storage, returning nonce of order + async function placeOrder( + user: SignerWithAddress, + side: Side, + comparison: Compare, + price: BigNumber, + delta: BigNumber, + maxFee = MAX_FEE, + referrer = constants.AddressZero, + interfaceFee = NO_INTERFACE_FEE, + ): Promise { + const order = { + side: side, + comparison: comparison, + price: price, + delta: delta, + maxFee: maxFee, + isSpent: false, + referrer: referrer, + ...interfaceFee, + } + advanceOrderId(user) + const orderId = nextOrderId[user.address] + await expect(manager.connect(user).placeOrder(market.address, orderId, order, TX_OVERRIDES)) + .to.emit(manager, 'TriggerOrderPlaced') + .withArgs(market.address, user.address, order, orderId) + + const storedOrder = await manager.orders(market.address, user.address, orderId) + compareOrders(storedOrder, order) + return orderId + } + + async function placeOrderWithSignature( + user: SignerWithAddress, + side: Side, + comparison: Compare, + price: BigNumber, + delta: BigNumber, + maxFee = MAX_FEE, + referrer = constants.AddressZero, + interfaceFee = NO_INTERFACE_FEE, + ): Promise { + advanceOrderId(user) + const message: PlaceOrderActionStruct = { + order: { + side: side, + comparison: comparison, + price: price, + delta: delta, + maxFee: maxFee, + isSpent: false, + referrer: referrer, + ...interfaceFee, + }, + ...createActionMessage(user.address), + } + const signature = await signPlaceOrderAction(user, verifier, message) + + await expect(manager.connect(keeper).placeOrderWithSignature(message, signature, TX_OVERRIDES)) + .to.emit(manager, 'TriggerOrderPlaced') + .withArgs(market.address, user.address, message.order, message.action.orderId) + + const storedOrder = await manager.orders(market.address, user.address, message.action.orderId) + compareOrders(storedOrder, message.order) + + return BigNumber.from(message.action.orderId) + } + + // set a realistic base gas fee to get realistic keeper compensation + async function setNextBlockBaseFee() { + await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x5F5E100']) // 0.1 gwei + } + + // running tests serially; can build a few scenario scripts and test multiple things within each script + before(async () => { + currentTime = BigNumber.from(await currentBlockTimestamp()) + const fixture = await getFixture(TX_OVERRIDES) + dsu = fixture.dsu + usdc = fixture.usdc + reserve = fixture.reserve + keeperOracle = fixture.keeperOracle + manager = fixture.manager + marketFactory = fixture.marketFactory + market = fixture.market + oracle = fixture.oracle + verifier = fixture.verifier + owner = fixture.owner + userA = fixture.userA + userB = fixture.userB + userC = fixture.userC + userD = fixture.userD + keeper = fixture.keeper + oracleFeeReceiver = fixture.oracleFeeReceiver + + nextOrderId[userA.address] = BigNumber.from(500) + nextOrderId[userB.address] = BigNumber.from(500) + + // commit a start price + await commitPrice(parse6decimal('4444')) + + // TODO: maybe just move this into the fixture? + await mockGasInfo() + }) + + beforeEach(async () => { + await setNextBlockBaseFee() + currentTime = BigNumber.from(await currentBlockTimestamp()) + keeperBalanceBefore = await dsu.balanceOf(keeper.address) + keeperEthBalanceBefore = await keeper.getBalance() + }) + + afterEach(async () => { + // ensure manager has no funds at rest + expect(await dsu.balanceOf(manager.address)).to.equal(constants.Zero) + }) + + after(async () => { + // reset to avoid impact to other tests + await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x1']) + }) + + // covers extension functionality; userA adds maker liquidity funding userB's long position + describe('empty market', () => { + it('constructs and initializes', async () => { + expect(await manager.DSU()).to.equal(dsu.address) + expect(await manager.marketFactory()).to.equal(marketFactory.address) + expect(await manager.verifier()).to.equal(verifier.address) + }) + + it('manager can verify a no-op action message', async () => { + // ensures any problems with message encoding are not caused by a common data type + const message = createActionMessage(userB.address).action + const signature = await signAction(userB, verifier, message) + + const managerSigner = await impersonate.impersonateWithBalance(manager.address, utils.parseEther('10')) + await expect(verifier.connect(managerSigner).verifyAction(message, signature, TX_OVERRIDES)) + .to.emit(verifier, 'NonceCancelled') + .withArgs(userB.address, message.common.nonce) + + expect(await verifier.nonces(userB.address, message.common.nonce)).to.eq(true) + }) + + it('single user can place order', async () => { + // userA places a 5k maker order + const orderId = await placeOrder(userA, Side.MAKER, Compare.LTE, parse6decimal('3993.6'), parse6decimal('55')) + expect(orderId).to.equal(BigNumber.from(501)) + + // orders not executed; no position + await ensureNoPosition(userA) + await ensureNoPosition(userB) + }) + + it('multiple users can place orders', async () => { + // if price drops below 3636.99, userA would have 10k maker position after both orders executed + let orderId = await placeOrder(userA, Side.MAKER, Compare.LTE, parse6decimal('3636.99'), parse6decimal('45')) + expect(orderId).to.equal(BigNumber.from(502)) + + // userB queues up a 2.5k long position; same order nonce as userA's first order + orderId = await placeOrder(userB, Side.LONG, Compare.GTE, parse6decimal('2222.22'), parse6decimal('2.5')) + expect(orderId).to.equal(BigNumber.from(501)) + + // orders not executed; no position + await ensureNoPosition(userA) + await ensureNoPosition(userB) + }) + + it('keeper cannot execute order when conditions not met', async () => { + const [, canExecute] = await manager.checkOrder(market.address, userA.address, 501) + expect(canExecute).to.be.false + + await expect( + manager.connect(keeper).executeOrder(market.address, userA.address, 501), + ).to.be.revertedWithCustomError(manager, 'ManagerCannotExecuteError') + }) + + it('keeper can execute orders', async () => { + // commit a price which should make all orders executable + await commitPrice(parse6decimal('2800')) + + // execute two maker orders and the long order + await executeOrder(userA, 501) + await commitPrice() + await executeOrder(userA, 502) + await commitPrice() + await executeOrder(userB, 501) + await commitPrice() + + // validate positions + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('100')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) + await market.connect(userA).settle(userA.address, TX_OVERRIDES) + + await checkCompensation(3) + }) + + it('user can place an order using a signed message', async () => { + const orderId = await placeOrderWithSignature( + userA, + Side.MAKER, + Compare.GTE, + parse6decimal('1000'), + parse6decimal('-10'), + ) + expect(orderId).to.equal(BigNumber.from(503)) + await checkCompensation(0) + + await executeOrder(userA, 503) + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) + await commitPrice(parse6decimal('2801')) + }) + + it('user can cancel an order', async () => { + // user places an order + const orderId = await placeOrder(userA, Side.MAKER, Compare.GTE, parse6decimal('1001'), parse6decimal('-7')) + expect(orderId).to.equal(BigNumber.from(504)) + + // user cancels the order nonce + await expect(manager.connect(userA).cancelOrder(market.address, orderId, TX_OVERRIDES)) + .to.emit(manager, 'TriggerOrderCancelled') + .withArgs(market.address, userA.address, orderId) + + const storedOrder = await manager.orders(market.address, userA.address, orderId) + expect(storedOrder.isSpent).to.be.true + + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) + }) + + it('user can cancel an order using a signed message', async () => { + // user places an order + const orderId = await placeOrder(userA, Side.MAKER, Compare.GTE, parse6decimal('1002'), parse6decimal('-6')) + expect(orderId).to.equal(BigNumber.from(505)) + + // user creates and signs a message to cancel the order nonce + const message = { + ...createActionMessage(userA.address, orderId), + } + const signature = await signCancelOrderAction(userA, verifier, message) + + // keeper handles the request + await expect(manager.connect(keeper).cancelOrderWithSignature(message, signature, TX_OVERRIDES)) + .to.emit(manager, 'TriggerOrderCancelled') + .withArgs(market.address, userA.address, orderId) + await checkCompensation(0) + + const storedOrder = await manager.orders(market.address, userA.address, orderId) + expect(storedOrder.isSpent).to.be.true + + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) + }) + + it('non-delegated signer cannot interact', async () => { + // userB signs a message to change userA's position + advanceOrderId(userA) + const message: PlaceOrderActionStruct = { + order: { + ...DEFAULT_TRIGGER_ORDER, + side: Side.MAKER, + comparison: Compare.GTE, + price: parse6decimal('1003'), + delta: parse6decimal('2'), + }, + ...createActionMessage(userA.address, nextMessageNonce(), userB.address), + } + const signature = await signPlaceOrderAction(userB, verifier, message) + + await expect( + manager.connect(keeper).placeOrderWithSignature(message, signature, TX_OVERRIDES), + ).to.be.revertedWithCustomError(verifier, 'VerifierInvalidSignerError') + }) + + it('delegated signer can interact', async () => { + // userA delegates userB + await marketFactory.connect(userA).updateSigner(userB.address, true, TX_OVERRIDES) + + // userB signs a message to change userA's position + advanceOrderId(userA) + const message: PlaceOrderActionStruct = { + order: { + ...DEFAULT_TRIGGER_ORDER, + side: Side.MAKER, + comparison: Compare.GTE, + price: parse6decimal('1004'), + delta: parse6decimal('3'), + }, + ...createActionMessage(userA.address, nextMessageNonce(), userB.address), + } + const signature = await signPlaceOrderAction(userB, verifier, message) + + await expect(manager.connect(keeper).placeOrderWithSignature(message, signature, TX_OVERRIDES)) + .to.emit(manager, 'TriggerOrderPlaced') + .withArgs(market.address, userA.address, message.order, message.action.orderId) + + const storedOrder = await manager.orders(market.address, userA.address, message.action.orderId) + compareOrders(storedOrder, message.order) + + // order was not executed, so no change in position + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) + await checkCompensation(0) + }) + + it('charges flat interface fee upon execution', async () => { + const positionBefore = await getPendingPosition(userA, Side.MAKER) + const interfaceBalanceBefore = await dsu.balanceOf(userC.address) + + // user A reduces their maker position through a GUI which charges an interface fee + const feeAmount = parse6decimal('3.5') + const interfaceFee = { + interfaceFee: { + amount: feeAmount, + receiver: userC.address, + fixedFee: true, + unwrap: false, + }, + } + const positionDelta = parse6decimal('-5') + const orderId = await placeOrder( + userA, + Side.MAKER, + Compare.LTE, + parse6decimal('2828.28'), + positionDelta, + MAX_FEE, + constants.AddressZero, + interfaceFee, + ) + expect(orderId).to.equal(BigNumber.from(508)) + + // keeper executes the order and user settles themselves + await executeOrder(userA, orderId) + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(positionBefore.add(positionDelta)) + await commitPrice() + await market.connect(userA).settle(userA.address, TX_OVERRIDES) + + // validate positions + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('85')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) + + // ensure fees were paid + await manager.connect(userC).claim(userC.address, false, TX_OVERRIDES) + expect(await dsu.balanceOf(userC.address)).to.equal(interfaceBalanceBefore.add(feeAmount.mul(1e12))) + await checkCompensation(1) + }) + + it('unwraps flat interface fee upon execution', async () => { + // user B increases their long position through a GUI which charges an interface fee + const feeAmount = parse6decimal('2.75') + const interfaceFee = { + interfaceFee: { + amount: feeAmount, + receiver: userC.address, + fixedFee: true, + unwrap: true, + }, + } + const positionDelta = parse6decimal('1.5') + const orderId = await placeOrder( + userB, + Side.LONG, + Compare.GTE, + parse6decimal('1900'), + positionDelta, + MAX_FEE, + constants.AddressZero, + interfaceFee, + ) + expect(orderId).to.equal(BigNumber.from(502)) + + // keeper executes the order and interface settles + await executeOrder(userB, orderId) + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('85')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('4')) // 2.5 + 1.5 + await commitPrice() + await market.connect(userC).settle(userB.address, TX_OVERRIDES) + + // ensure fees were paid + await manager.connect(userC).claim(userC.address, true, TX_OVERRIDES) + expect(await usdc.balanceOf(userC.address)).to.equal(feeAmount) + await checkCompensation(1) + }) + + it('unwraps notional interface fee upon execution', async () => { + const interfaceBalanceBefore = await usdc.balanceOf(userC.address) + + // userB increases their long position through a GUI which charges a notional interface fee + const interfaceFee = { + interfaceFee: { + amount: parse6decimal('0.0055'), + receiver: userC.address, + fixedFee: false, + unwrap: true, + }, + } + const orderId = await placeOrder( + userB, + Side.LONG, + Compare.GTE, + parse6decimal('0.01'), + parse6decimal('3'), + MAX_FEE, + constants.AddressZero, + interfaceFee, + ) + expect(orderId).to.equal(BigNumber.from(503)) + + // keeper executes the order and user settles + expect((await oracle.latest()).price).to.equal(parse6decimal('2801')) + // delta * price * fee amount = 3 * 2801 * 0.0055 + const expectedInterfaceFee = parse6decimal('46.2165') + await executeOrder(userB, orderId, expectedInterfaceFee) + expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('85')) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('7')) // 4 + 3 + await commitPrice() + await market.connect(userB).settle(userB.address, TX_OVERRIDES) + + // ensure fees were paid + await manager.connect(userC).claim(userC.address, true, TX_OVERRIDES) + expect(await usdc.balanceOf(userC.address)).to.equal(interfaceBalanceBefore.add(expectedInterfaceFee)) + await checkCompensation(1) + }) + + it('users can close positions', async () => { + // can close directly + let orderId = await placeOrder(userA, Side.MAKER, Compare.GTE, constants.Zero, MAGIC_VALUE_CLOSE_POSITION) + expect(orderId).to.equal(BigNumber.from(509)) + + // can close using a signed message + orderId = await placeOrderWithSignature( + userB, + Side.LONG, + Compare.LTE, + parse6decimal('4000'), + MAGIC_VALUE_CLOSE_POSITION, + ) + expect(orderId).to.equal(BigNumber.from(504)) + + // keeper closes the taker position before removing liquidity + await executeOrder(userB, 504) + await commitPrice() + await executeOrder(userA, 509) + await commitPrice() + + // settle and confirm positions are closed + await market.settle(userA.address, TX_OVERRIDES) + await ensureNoPosition(userA) + await market.settle(userB.address, TX_OVERRIDES) + await ensureNoPosition(userB) + }) + }) + + // tests interaction with markets; again userA has a maker position, userB has a long position, + // userC and userD interact only with trigger orders + describe('funded market', () => { + async function changePosition( + user: SignerWithAddress, + newMaker: BigNumberish = constants.MaxUint256, + newLong: BigNumberish = constants.MaxUint256, + newShort: BigNumberish = constants.MaxUint256, + ): Promise { + const tx = await market + .connect(user) + ['update(address,uint256,uint256,uint256,int256,bool)']( + user.address, + newMaker, + newLong, + newShort, + 0, + false, + TX_OVERRIDES, + ) + return (await getEventArguments(tx, 'OrderCreated')).order.timestamp + } + + before(async () => { + // ensure no positions were carried over from previous test suite + await ensureNoPosition(userA) + await ensureNoPosition(userB) + + await changePosition(userA, parse6decimal('10'), 0, 0) + await commitPrice(parse6decimal('2000')) + await market.settle(userA.address, TX_OVERRIDES) + + nextOrderId[userA.address] = BigNumber.from(600) + nextOrderId[userB.address] = BigNumber.from(600) + nextOrderId[userC.address] = BigNumber.from(600) + nextOrderId[userD.address] = BigNumber.from(600) + }) + + afterEach(async () => { + await checkCompensation(1) + }) + + it('can execute an order with pending position before oracle request fulfilled', async () => { + // userB has an unsettled long 1.2 position + await changePosition(userB, 0, parse6decimal('1.2'), 0) + expect((await market.positions(userB.address)).long).to.equal(0) + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('1.2')) + + // userB places an order to go long 0.8 more, and keeper executes it + const orderId = await placeOrder(userB, Side.LONG, Compare.LTE, parse6decimal('2013'), parse6decimal('0.8')) + expect(orderId).to.equal(BigNumber.from(601)) + advanceBlock() + const orderTimestamp = await executeOrder(userB, 601) + + // userB still has no settled position + expect((await market.positions(userB.address)).long).to.equal(0) + // but the order should increase their pending position to 2 + expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.0')) + + // commit price for both their 1.2 position and the 0.8 added through trigger order + await commitPrice(parse6decimal('2001.01'), await keeperOracle.next()) + await commitPrice(parse6decimal('2001.02'), orderTimestamp) + // settle userB and check position + await market.settle(userB.address, TX_OVERRIDES) + expect((await market.positions(userB.address)).long).to.equal(parse6decimal('2.0')) + }) + + it('can execute an order with pending position after oracle request fulfilled', async () => { + // userC has an unsettled short 0.3 position + await changePosition(userC, 0, 0, parse6decimal('1.3')) + expect((await market.positions(userC.address)).short).to.equal(0) + expect(await getPendingPosition(userC, Side.SHORT)).to.equal(parse6decimal('1.3')) + + // userC places an order to go short 1.2 more, and keeper executes it + const orderId = await placeOrder(userC, Side.SHORT, Compare.GTE, parse6decimal('1999.97'), parse6decimal('1.2')) + expect(orderId).to.equal(BigNumber.from(601)) + advanceBlock() + const orderTimestamp = await executeOrder(userC, 601) + + // prices are committed for both versions + await commitPrice(parse6decimal('2002.03'), await keeperOracle.next()) + await commitPrice(parse6decimal('2002.04'), orderTimestamp) + + // userC still has no settled position + expect((await market.positions(userC.address)).long).to.equal(0) + // but the order should increase their short position to 2.5 + expect(await getPendingPosition(userC, Side.SHORT)).to.equal(parse6decimal('2.5')) + + // after settling userC, they should be short 2.5 + await market.settle(userC.address, TX_OVERRIDES) + expect((await market.positions(userC.address)).short).to.equal(parse6decimal('2.5')) + }) + + it('can execute an order once market conditions allow', async () => { + // userD places an order to go long 3 once price dips below 2000 + const triggerPrice = parse6decimal('2000') + const orderId = await placeOrder(userD, Side.LONG, Compare.LTE, triggerPrice, parse6decimal('3')) + expect(orderId).to.equal(BigNumber.from(601)) + advanceBlock() + + // the order is not yet executable + const [, canExecuteBefore] = await manager.checkOrder(market.address, userD.address, orderId) + expect(canExecuteBefore).to.be.false + + // time passes, other users interact with market + let positionA = (await market.positions(userA.address)).maker + let positionC = (await market.positions(userC.address)).short + let marketPrice = (await oracle.latest()).price + + while (marketPrice.gt(triggerPrice)) { + // two users change their position + positionA = positionA.add(parse6decimal('0.05')) + const timestampA = await changePosition(userA, positionA, 0, 0) + positionC = positionC.sub(parse6decimal('0.04')) + const timestampC = await changePosition(userC, 0, 0, positionC) + + // oracle versions fulfilled + marketPrice = marketPrice.sub(parse6decimal('0.35')) + await commitPrice(marketPrice, timestampA) + await commitPrice(marketPrice, timestampC) + + // advance 5 minutes + await increase(60 * 5) + advanceBlock() + await setNextBlockBaseFee() + + // userA settled each time + await market.settle(userA.address, TX_OVERRIDES) + } + // userC settled after considerable time + await market.settle(userC.address, TX_OVERRIDES) + + // confirm order is now executable + const [, canExecuteAfter] = await manager.checkOrder(market.address, userD.address, orderId) + expect(canExecuteAfter).to.be.true + + // execute order + const orderTimestamp = await executeOrder(userD, 601) + expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('3')) + + // fulfill oracle version and settle + await commitPrice(parse6decimal('2000.1'), orderTimestamp) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userC.address)).short).to.equal(parse6decimal('2.26')) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('3')) + }) + + it('market reverts when attempting to close an unsettled positive position', async () => { + // userD submits order extending their long position, which keeper executes + let orderId = await placeOrder( + userD, + Side.LONG, + Compare.GTE, + parse6decimal('0.01'), + parse6decimal('1.5'), + MAX_FEE, + constants.AddressZero, + NO_INTERFACE_FEE, + ) + expect(orderId).to.equal(BigNumber.from(602)) + const longOrderTimestamp = await executeOrder(userD, orderId) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('3')) + expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('4.5')) + + // before settling, userD closes their long position + orderId = await placeOrder(userD, Side.LONG, Compare.LTE, parse6decimal('9999'), MAGIC_VALUE_CLOSE_POSITION) + expect(orderId).to.equal(BigNumber.from(603)) + + await expect( + manager.connect(keeper).executeOrder(market.address, userD.address, orderId, TX_OVERRIDES), + ).to.be.revertedWithCustomError(market, 'MarketOverCloseError') + + // keeper commits price, settles the long order + await commitPrice(parse6decimal('2000.2'), longOrderTimestamp) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('4.5')) + }) + + it('market handles attempt to close an unsettled negative position', async () => { + // userD submits order reducing their long position, which keeper executes + let orderId = await placeOrder( + userD, + Side.LONG, + Compare.GTE, + parse6decimal('0.01'), + parse6decimal('-0.5'), + MAX_FEE, + constants.AddressZero, + NO_INTERFACE_FEE, + ) + expect(orderId).to.equal(BigNumber.from(604)) + const reduceOrderTimestamp = await executeOrder(userD, orderId) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('4.5')) + expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('4')) + + // before settling, userD attempts to close their long position + orderId = await placeOrder(userD, Side.LONG, Compare.LTE, parse6decimal('9999'), MAGIC_VALUE_CLOSE_POSITION) + expect(orderId).to.equal(BigNumber.from(605)) + const closeOrderTimestamp = await executeOrder(userD, orderId) + + // keeper commits price, settles the long order + await commitPrice(parse6decimal('2000.31'), reduceOrderTimestamp) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('4')) + + // keeper commits another price, settles the close order + await commitPrice(parse6decimal('2000.32'), closeOrderTimestamp) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('0')) + }) + + it('charges notional interface fee on whole position when closing', async () => { + const interfaceBalanceBefore = await dsu.balanceOf(userB.address) + + // userD starts with a long 3 position + await changePosition(userD, 0, parse6decimal('3'), 0) + await commitPrice(parse6decimal('2000.4')) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('3')) + expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('3')) + + // userD closes their long position + const interfaceFee = { + interfaceFee: { + amount: parse6decimal('0.00654'), + receiver: userB.address, + fixedFee: false, + unwrap: false, + }, + } + const orderId = await placeOrder( + userD, + Side.LONG, + Compare.LTE, + parse6decimal('9999'), + MAGIC_VALUE_CLOSE_POSITION, + MAX_FEE, + constants.AddressZero, + interfaceFee, + ) + expect(orderId).to.equal(BigNumber.from(606)) + + const expectedInterfaceFee = parse6decimal('39.247848') // position * price * fee + const closeOrderTimestamp = await executeOrder(userD, orderId, expectedInterfaceFee) + expect(await getPendingPosition(userD, Side.LONG)).to.equal(constants.Zero) + + // ensure fees were paid + await manager.connect(userB).claim(userB.address, false, TX_OVERRIDES) + expect(await dsu.balanceOf(userB.address)).to.equal(interfaceBalanceBefore.add(expectedInterfaceFee.mul(1e12))) + + // settle before next test + await commitPrice(parse6decimal('2000.4'), closeOrderTimestamp) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('0')) + }) + + it('charges notional interface fee when closing with a pending negative position', async () => { + const interfaceBalanceBefore = await dsu.balanceOf(userB.address) + + // userD starts with a short 2 position + await changePosition(userD, 0, 0, parse6decimal('2')) + await commitPrice(parse6decimal('2000.5')) + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).short).to.equal(parse6decimal('2')) + + // userD reduces their position by 0.35 but does not settle + const negOrderTimestamp = await changePosition(userD, 0, 0, parse6decimal('1.65')) + expect(await getPendingPosition(userD, Side.SHORT)).to.equal(parse6decimal('1.65')) + + // userD closes their short position + const interfaceFee = { + interfaceFee: { + amount: parse6decimal('0.0051'), + receiver: userB.address, + fixedFee: false, + unwrap: false, + }, + } + const orderId = await placeOrder( + userD, + Side.SHORT, + Compare.LTE, + parse6decimal('9999'), + MAGIC_VALUE_CLOSE_POSITION, + MAX_FEE, + constants.AddressZero, + interfaceFee, + ) + expect(orderId).to.equal(BigNumber.from(607)) + + // position * price * fee = 1.65 * 2000.5 * 0.0051 + const expectedInterfaceFee = parse6decimal('16.8342075') + await setNextBlockBaseFee() + const closeOrderTimestamp = await executeOrder(userD, orderId, expectedInterfaceFee) + expect(await getPendingPosition(userD, Side.SHORT)).to.equal(constants.Zero) + + // ensure fees were paid + await manager.connect(userB).claim(userB.address, false, TX_OVERRIDES) + expect(await dsu.balanceOf(userB.address)).to.equal(interfaceBalanceBefore.add(expectedInterfaceFee.mul(1e12))) + + // settle before next test + await commitPrice(parse6decimal('2000.4'), negOrderTimestamp) + await commitPrice(parse6decimal('2000.4'), closeOrderTimestamp) + await setNextBlockBaseFee() + await market.settle(userD.address, TX_OVERRIDES) + expect((await market.positions(userD.address)).long).to.equal(parse6decimal('0')) + }) + }) + }) +} diff --git a/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts b/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts index e8e834353..3b0b2c2c9 100644 --- a/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts +++ b/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts @@ -1,1026 +1,292 @@ import { expect } from 'chai' -import { BigNumber, BigNumberish, constants, utils } from 'ethers' -import HRE from 'hardhat' -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' - +import { BigNumber, CallOverrides, constants, utils } from 'ethers' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { smock } from '@defi-wonderland/smock' +import HRE from 'hardhat' -import { advanceBlock, currentBlockTimestamp, increase } from '../../../common/testutil/time' -import { getEventArguments, getTimestamp } from '../../../common/testutil/transaction' -import { parse6decimal } from '../../../common/testutil/types' - -import { IERC20Metadata, IMarketFactory, IMarket, IOracleProvider } from '@equilibria/perennial-v2/types/generated' -import { IKeeperOracle, IOracleFactory } from '@equilibria/perennial-v2-oracle/types/generated' +import { IMarket, MarketFactory, MarketFactory__factory } from '@equilibria/perennial-v2/types/generated' +import { + IKeeperOracle, + IOracleFactory, + IOracleProvider, + KeeperOracle__factory, + OracleFactory, + PythFactory, + PythFactory__factory, + GasOracle__factory, +} from '@equilibria/perennial-v2-oracle/types/generated' +import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { ArbGasInfo, - IEmptySetReserve, IEmptySetReserve__factory, + IERC20Metadata, IERC20Metadata__factory, - IOrderVerifier, - Manager_Arbitrum, + IManager, + IMarketFactory, + IVerifier, Manager_Arbitrum__factory, OrderVerifier__factory, } from '../../types/generated' - -import { signAction, signCancelOrderAction, signPlaceOrderAction } from '../helpers/eip712' -import { createMarketETH, deployProtocol, deployPythOracleFactory, fundWalletDSU } from '../helpers/arbitrumHelpers' -import { - Compare, - compareOrders, - DEFAULT_TRIGGER_ORDER, - MAGIC_VALUE_CLOSE_POSITION, - orderFromStructOutput, - Side, -} from '../helpers/order' -import { transferCollateral } from '../helpers/marketHelpers' -import { advanceToPrice } from '../helpers/oracleHelpers' -import { PlaceOrderActionStruct } from '../../types/generated/contracts/Manager' -import { Address } from 'hardhat-deploy/dist/types' -import { smock } from '@defi-wonderland/smock' import { impersonate } from '../../../common/testutil' +import { Address } from 'hardhat-deploy/dist/types' +import { parse6decimal } from '../../../common/testutil/types' +import { createPythOracle, deployOracleFactory } from '../helpers/oracleHelpers' +import { createMarket, deployMarketImplementation, transferCollateral } from '../helpers/marketHelpers' +import { FixtureVars } from '../helpers/setupHelpers' +import { RunManagerTests } from './Manager.test' const { ethers } = HRE -const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' // price feed used for keeper compensation +const DSU_ADDRESS = '0x52C64b8998eB7C80b6F526E99E29ABdcC86B841b' // Digital Standard Unit, an 18-decimal token +const DSU_HOLDER = '0x90a664846960aafa2c164605aebb8e9ac338f9a0' // Perennial Market has 4.7mm at height 243648015 const DSU_RESERVE = '0x0d49c416103Cbd276d9c3cd96710dB264e3A0c27' const USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' // Arbitrum native USDC, a 6-decimal token -const MAX_FEE = utils.parseEther('0.88') +const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' +const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' -const NO_INTERFACE_FEE = { - interfaceFee: { - amount: constants.Zero, - receiver: constants.AddressZero, - fixedFee: false, - unwrap: false, - }, -} - -// because we called hardhat_setNextBlockBaseFeePerGas, need this when running tests under coverage -const TX_OVERRIDES = { maxPriorityFeePerGas: 0, maxFeePerGas: 150_000_000 } - -describe('Manager_Arbitrum', () => { - let dsu: IERC20Metadata - let usdc: IERC20Metadata - let reserve: IEmptySetReserve - let keeperOracle: IKeeperOracle - let manager: Manager_Arbitrum - let marketFactory: IMarketFactory - let market: IMarket - let oracle: IOracleProvider - let verifier: IOrderVerifier - let owner: SignerWithAddress - let userA: SignerWithAddress - let userB: SignerWithAddress - let userC: SignerWithAddress - let userD: SignerWithAddress - let keeper: SignerWithAddress - let oracleFeeReceiver: SignerWithAddress - let currentTime: BigNumber - let keeperBalanceBefore: BigNumber - let keeperEthBalanceBefore: BigNumber - let lastMessageNonce = 0 - let lastPriceCommitted: BigNumber - const nextOrderId: { [key: string]: BigNumber } = {} - - function advanceOrderId(user: SignerWithAddress) { - nextOrderId[user.address] = nextOrderId[user.address].add(BigNumber.from(1)) - } - - const fixture = async () => { - // deploy the protocol and create a market - ;[owner, userA, userB, userC, userD, keeper, oracleFeeReceiver] = await ethers.getSigners() - let oracleFactory: IOracleFactory - ;[marketFactory, dsu, oracleFactory] = await deployProtocol(owner) - usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) - reserve = IEmptySetReserve__factory.connect(DSU_RESERVE, owner) - const pythOracleFactory = await deployPythOracleFactory(owner, oracleFactory) - ;[market, oracle, keeperOracle] = await createMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu) - - // deploy the order manager - verifier = await new OrderVerifier__factory(owner).deploy(marketFactory.address) - manager = await new Manager_Arbitrum__factory(owner).deploy( - USDC_ADDRESS, - dsu.address, - DSU_RESERVE, - marketFactory.address, - verifier.address, - ) - - const keepConfig = { - multiplierBase: ethers.utils.parseEther('1'), - bufferBase: 1_000_000, // buffer for withdrawing keeper fee from market - multiplierCalldata: 0, - bufferCalldata: 0, - } - const keepConfigBuffered = { - multiplierBase: ethers.utils.parseEther('1.05'), - bufferBase: 1_500_000, // for price commitment - multiplierCalldata: ethers.utils.parseEther('1.05'), - bufferCalldata: 35_200, - } - await manager.initialize(CHAINLINK_ETH_USD_FEED, keepConfig, keepConfigBuffered) - - // commit a start price - await commitPrice(parse6decimal('4444')) - - // fund accounts and deposit all into market - const amount = parse6decimal('100000') - await setupUser(userA, amount) - await setupUser(userB, amount) - await setupUser(userC, amount) - await setupUser(userD, amount) - } - - // prepares an account for use with the market and manager - async function setupUser(user: SignerWithAddress, amount: BigNumber) { - // funds, approves, and deposits DSU into the market - await fundWalletDSU(user, amount.mul(1e12)) - await dsu.connect(user).approve(market.address, amount.mul(1e12)) - await transferCollateral(user, market, amount) - - // allows manager to interact with markets on the user's behalf - await marketFactory.connect(user).updateOperator(manager.address, true) - } - - async function checkCompensation(priceCommitments = 0) { - await expect(manager.connect(keeper).claim(keeper.address, false, TX_OVERRIDES)) - const keeperFeesPaid = (await dsu.balanceOf(keeper.address)).sub(keeperBalanceBefore) - let keeperEthSpentOnGas = keeperEthBalanceBefore.sub(await keeper.getBalance()) - - // If TXes in test required outside price commitments, compensate the keeper for them. - // Note that calls to `commitPrice` in this module do not consume keeper gas. - keeperEthSpentOnGas = keeperEthSpentOnGas.add(utils.parseEther('0.0000644306').mul(priceCommitments)) - - // cost of transaction - const keeperGasCostInUSD = keeperEthSpentOnGas.mul(2603) - // keeper should be compensated between 100-200% of actual gas cost - expect(keeperFeesPaid).to.be.within(keeperGasCostInUSD, keeperGasCostInUSD.mul(2)) - } - - // commits an oracle version and advances time 10 seconds - async function commitPrice( - price = lastPriceCommitted, - timestamp: BigNumber | undefined = undefined, - ): Promise { - if (!timestamp) timestamp = await oracle.current() - - lastPriceCommitted = price - return advanceToPrice(keeperOracle, oracleFeeReceiver, timestamp!, price, TX_OVERRIDES) - } - - function createActionMessage( - userAddress: Address, - nonce = nextMessageNonce(), - signerAddress = userAddress, - expiresInSeconds = 24, - ) { - return { - action: { - market: market.address, - orderId: nextOrderId[userAddress], - maxFee: MAX_FEE, - common: { - account: userAddress, - signer: signerAddress, - domain: manager.address, - nonce: nonce, - group: 0, - expiry: currentTime.add(expiresInSeconds), - }, - }, - } - } - - async function ensureNoPosition(user: SignerWithAddress) { - const position = await market.positions(user.address) - expect(position.maker).to.equal(0) - expect(position.long).to.equal(0) - expect(position.short).to.equal(0) - const pending = await market.pendings(user.address) - expect(pending.makerPos.sub(pending.makerNeg)).to.equal(0) - expect(pending.longPos.sub(pending.longNeg)).to.equal(0) - expect(pending.shortPos.sub(pending.shortNeg)).to.equal(0) - } +const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' // price feed used for keeper compensation - // executes an order as keeper - async function executeOrder( - user: SignerWithAddress, - orderId: BigNumberish, - expectedInterfaceFee: BigNumber | undefined = undefined, - ): Promise { - // ensure order is executable - const [order, canExecute] = await manager.checkOrder(market.address, user.address, orderId) - expect(canExecute).to.be.true +// creates an ETH market using a locally deployed factory and oracle +export async function createMarketETH( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythOracleFactory: PythFactory, + marketFactory: IMarketFactory, + dsu: IERC20Metadata, + overrides?: CallOverrides, +): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { + // Create oracles needed to support the market + const [keeperOracle, oracle] = await createPythOracle( + owner, + oracleFactory, + pythOracleFactory, + PYTH_ETH_USD_PRICE_FEED, + 'ETH-USD', + overrides, + ) + // Create the market in which user or collateral account may interact + const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) + await keeperOracle.register(oracle.address) + await oracle.register(market.address) + return [market, oracle, keeperOracle] +} - // validate event - const tx = await manager.connect(keeper).executeOrder(market.address, user.address, orderId, TX_OVERRIDES) - // set the order's spent flag true to validate event - const spentOrder = { ...orderFromStructOutput(order), isSpent: true } - await expect(tx) - .to.emit(manager, 'TriggerOrderExecuted') - .withArgs(market.address, user.address, spentOrder, orderId) - .to.emit(market, 'OrderCreated') - .withArgs(user.address, anyValue, anyValue, constants.AddressZero, order.referrer, constants.AddressZero) - if (order.interfaceFee.amount.gt(0)) { - if (!expectedInterfaceFee && order.interfaceFee.fixedFee) { - expectedInterfaceFee = order.interfaceFee.amount - } - if (expectedInterfaceFee) { - await expect(tx) - .to.emit(manager, 'TriggerOrderInterfaceFeeCharged') - .withArgs(user.address, market.address, order.interfaceFee) - if (order.interfaceFee.unwrap) { - await expect(tx) - .to.emit(dsu, 'Transfer') - .withArgs(market.address, manager.address, expectedInterfaceFee.mul(1e12)) - } else { - await expect(tx) - .to.emit(dsu, 'Transfer') - .withArgs(market.address, manager.address, expectedInterfaceFee.mul(1e12)) - } - } - } - const timestamp = await getTimestamp(tx) - // ensure trigger order was marked as spent - const deletedOrder = await manager.orders(market.address, user.address, orderId) - expect(deletedOrder.isSpent).to.be.true +// Deploys the market factory and configures default protocol parameters +async function deployMarketFactory( + owner: SignerWithAddress, + pauser: SignerWithAddress, + oracleFactoryAddress: Address, + verifierAddress: Address, + marketImplAddress: Address, +): Promise { + const marketFactory = await new MarketFactory__factory(owner).deploy( + oracleFactoryAddress, + verifierAddress, + marketImplAddress, + ) + await marketFactory.connect(owner).initialize() + + // Set protocol parameters + await marketFactory.updatePauser(pauser.address) + await marketFactory.updateParameter({ + maxFee: parse6decimal('0.01'), + maxLiquidationFee: parse6decimal('20'), + maxCut: parse6decimal('0.50'), + maxRate: parse6decimal('10.00'), + minMaintenance: parse6decimal('0.01'), + minEfficiency: parse6decimal('0.1'), + referralFee: 0, + minScale: parse6decimal('0.001'), + maxStaleAfter: 7200, + }) - return BigNumber.from(timestamp) - } + return marketFactory +} - async function getPendingPosition(user: SignerWithAddress, side: Side) { - const position = await market.positions(user.address) - const pending = await market.pendings(user.address) +// Deploys OracleFactory and then MarketFactory +export async function deployProtocol( + owner: SignerWithAddress, +): Promise<[IMarketFactory, IERC20Metadata, IOracleFactory]> { + // Deploy the oracle factory, which markets created by the market factory will query + const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) + const oracleFactory = await deployOracleFactory(owner) + + // Deploy the market factory and authorize it with the oracle factory + const marketVerifier = await new Verifier__factory(owner).deploy() + const marketFactory = await deployProtocolForOracle(owner, oracleFactory, marketVerifier) + return [marketFactory, dsu, oracleFactory] +} - let actualPos: BigNumber - let pendingPos: BigNumber - switch (side) { - case Side.MAKER: - actualPos = position.maker - pendingPos = pending.makerPos.sub(pending.makerNeg) - break - case Side.LONG: - actualPos = position.long - pendingPos = pending.longPos.sub(pending.longNeg) - break - case Side.SHORT: - actualPos = position.short - pendingPos = pending.shortPos.sub(pending.shortNeg) - break - default: - throw new Error('Unexpected side') - } +// Deploys a market implementation and the MarketFactory for a provided oracle factory +async function deployProtocolForOracle( + owner: SignerWithAddress, + oracleFactory: OracleFactory, + verifier: IVerifier, +): Promise { + // Deploy protocol contracts + const marketImpl = await deployMarketImplementation(owner, verifier.address) + const marketFactory = await deployMarketFactory( + owner, + owner, + oracleFactory.address, + verifier.address, + marketImpl.address, + ) + return marketFactory +} - return actualPos.add(pendingPos) - } +export async function deployPythOracleFactory( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, +): Promise { + const commitmentGasOracle = await new GasOracle__factory(owner).deploy( + CHAINLINK_ETH_USD_FEED, + 8, + 1_000_000, + utils.parseEther('1.02'), + 1_000_000, + 0, + 0, + 0, + ) + const settlementGasOracle = await new GasOracle__factory(owner).deploy( + CHAINLINK_ETH_USD_FEED, + 8, + 200_000, + utils.parseEther('1.02'), + 500_000, + 0, + 0, + 0, + ) + + // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices + const keeperOracleImpl = await new KeeperOracle__factory(owner).deploy(60) + const pythOracleFactory = await new PythFactory__factory(owner).deploy( + PYTH_ADDRESS, + commitmentGasOracle.address, + settlementGasOracle.address, + keeperOracleImpl.address, + ) + await pythOracleFactory.initialize(oracleFactory.address) + await pythOracleFactory.updateParameter(1, 0, 4, 10) + await oracleFactory.register(pythOracleFactory.address) + return pythOracleFactory +} - function nextMessageNonce(): BigNumber { - return BigNumber.from(++lastMessageNonce) - } +// TODO: consider rolling this into setupUser +export async function fundWalletDSU( + wallet: SignerWithAddress, + amount: BigNumber, + overrides?: CallOverrides, +): Promise { + const dsuOwner = await impersonate.impersonateWithBalance(DSU_HOLDER, utils.parseEther('10')) + const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, dsuOwner) + + expect(await dsu.balanceOf(DSU_HOLDER)).to.be.greaterThan(amount) + await dsu.transfer(wallet.address, amount, overrides ?? {}) +} - // submits a trigger order, validating event and storage, returning nonce of order - async function placeOrder( - user: SignerWithAddress, - side: Side, - comparison: Compare, - price: BigNumber, - delta: BigNumber, - maxFee = MAX_FEE, - referrer = constants.AddressZero, - interfaceFee = NO_INTERFACE_FEE, - ): Promise { - const order = { - side: side, - comparison: comparison, - price: price, - delta: delta, - maxFee: maxFee, - isSpent: false, - referrer: referrer, - ...interfaceFee, - } - advanceOrderId(user) - const orderId = nextOrderId[user.address] - await expect(manager.connect(user).placeOrder(market.address, orderId, order, TX_OVERRIDES)) - .to.emit(manager, 'TriggerOrderPlaced') - .withArgs(market.address, user.address, order, orderId) +// prepares an account for use with the market and manager +async function setupUser( + dsu: IERC20Metadata, + marketFactory: IMarketFactory, + market: IMarket, + manager: IManager, + user: SignerWithAddress, + amount: BigNumber, +) { + // funds, approves, and deposits DSU into the market + await fundWalletDSU(user, amount.mul(1e12)) + await dsu.connect(user).approve(market.address, amount.mul(1e12)) + await transferCollateral(user, market, amount) + + // allows manager to interact with markets on the user's behalf + await marketFactory.connect(user).updateOperator(manager.address, true) +} - const storedOrder = await manager.orders(market.address, user.address, orderId) - compareOrders(storedOrder, order) - return orderId +const fixture = async (): Promise => { + // deploy the protocol and create a market + const [owner, userA, userB, userC, userD, keeper, oracleFeeReceiver] = await ethers.getSigners() + const [marketFactory, dsu, oracleFactory] = await deployProtocol(owner) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) + const reserve = IEmptySetReserve__factory.connect(DSU_RESERVE, owner) + const pythOracleFactory = await deployPythOracleFactory(owner, oracleFactory) + const [market, oracle, keeperOracle] = await createMarketETH( + owner, + oracleFactory, + pythOracleFactory, + marketFactory, + dsu, + ) + + // deploy the order manager + const verifier = await new OrderVerifier__factory(owner).deploy(marketFactory.address) + const manager = await new Manager_Arbitrum__factory(owner).deploy( + USDC_ADDRESS, + dsu.address, + DSU_RESERVE, + marketFactory.address, + verifier.address, + ) + + const keepConfig = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 1_000_000, // buffer for withdrawing keeper fee from market + multiplierCalldata: 0, + bufferCalldata: 0, } - - async function placeOrderWithSignature( - user: SignerWithAddress, - side: Side, - comparison: Compare, - price: BigNumber, - delta: BigNumber, - maxFee = MAX_FEE, - referrer = constants.AddressZero, - interfaceFee = NO_INTERFACE_FEE, - ): Promise { - advanceOrderId(user) - const message: PlaceOrderActionStruct = { - order: { - side: side, - comparison: comparison, - price: price, - delta: delta, - maxFee: maxFee, - isSpent: false, - referrer: referrer, - ...interfaceFee, - }, - ...createActionMessage(user.address), - } - const signature = await signPlaceOrderAction(user, verifier, message) - - await expect(manager.connect(keeper).placeOrderWithSignature(message, signature, TX_OVERRIDES)) - .to.emit(manager, 'TriggerOrderPlaced') - .withArgs(market.address, user.address, message.order, message.action.orderId) - - const storedOrder = await manager.orders(market.address, user.address, message.action.orderId) - compareOrders(storedOrder, message.order) - - return BigNumber.from(message.action.orderId) + const keepConfigBuffered = { + multiplierBase: ethers.utils.parseEther('1.05'), + bufferBase: 1_500_000, // for price commitment + multiplierCalldata: ethers.utils.parseEther('1.05'), + bufferCalldata: 35_200, } - - // set a realistic base gas fee to get realistic keeper compensation - async function setNextBlockBaseFee() { - await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x5F5E100']) // 0.1 gwei + await manager.initialize(CHAINLINK_ETH_USD_FEED, keepConfig, keepConfigBuffered) + + // TODO: can user setup be handled by the test in such a way that the test calls loadFixture + // after some nested setup? + // fund accounts and deposit all into market + const amount = parse6decimal('100000') + await setupUser(dsu, marketFactory, market, manager, userA, amount) + await setupUser(dsu, marketFactory, market, manager, userB, amount) + await setupUser(dsu, marketFactory, market, manager, userC, amount) + await setupUser(dsu, marketFactory, market, manager, userD, amount) + + return { + dsu, + usdc, + reserve, + keeperOracle, + manager, + marketFactory, + market, + oracle, + verifier, + owner, + userA, + userB, + userC, + userD, + keeper, + oracleFeeReceiver, } +} - // running tests serially; can build a few scenario scripts and test multiple things within each script - before(async () => { - currentTime = BigNumber.from(await currentBlockTimestamp()) - await loadFixture(fixture) - nextOrderId[userA.address] = BigNumber.from(500) - nextOrderId[userB.address] = BigNumber.from(500) - - // Hardhat fork does not support Arbitrum built-ins - await smock.fake('ArbGasInfo', { - address: '0x000000000000000000000000000000000000006C', - }) - }) - - beforeEach(async () => { - await setNextBlockBaseFee() - currentTime = BigNumber.from(await currentBlockTimestamp()) - keeperBalanceBefore = await dsu.balanceOf(keeper.address) - keeperEthBalanceBefore = await keeper.getBalance() - }) - - afterEach(async () => { - // ensure manager has no funds at rest - expect(await dsu.balanceOf(manager.address)).to.equal(constants.Zero) - }) - - after(async () => { - // reset to avoid impact to other tests - await HRE.ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', ['0x1']) - }) - - // covers extension functionality; userA adds maker liquidity funding userB's long position - describe('empty market', () => { - it('constructs and initializes', async () => { - expect(await manager.DSU()).to.equal(dsu.address) - expect(await manager.marketFactory()).to.equal(marketFactory.address) - expect(await manager.verifier()).to.equal(verifier.address) - }) - - it('manager can verify a no-op action message', async () => { - // ensures any problems with message encoding are not caused by a common data type - const message = createActionMessage(userB.address).action - const signature = await signAction(userB, verifier, message) - - const managerSigner = await impersonate.impersonateWithBalance(manager.address, utils.parseEther('10')) - await expect(verifier.connect(managerSigner).verifyAction(message, signature, TX_OVERRIDES)) - .to.emit(verifier, 'NonceCancelled') - .withArgs(userB.address, message.common.nonce) - - expect(await verifier.nonces(userB.address, message.common.nonce)).to.eq(true) - }) - - it('single user can place order', async () => { - // userA places a 5k maker order - const orderId = await placeOrder(userA, Side.MAKER, Compare.LTE, parse6decimal('3993.6'), parse6decimal('55')) - expect(orderId).to.equal(BigNumber.from(501)) - - // orders not executed; no position - await ensureNoPosition(userA) - await ensureNoPosition(userB) - }) - - it('multiple users can place orders', async () => { - // if price drops below 3636.99, userA would have 10k maker position after both orders executed - let orderId = await placeOrder(userA, Side.MAKER, Compare.LTE, parse6decimal('3636.99'), parse6decimal('45')) - expect(orderId).to.equal(BigNumber.from(502)) - - // userB queues up a 2.5k long position; same order nonce as userA's first order - orderId = await placeOrder(userB, Side.LONG, Compare.GTE, parse6decimal('2222.22'), parse6decimal('2.5')) - expect(orderId).to.equal(BigNumber.from(501)) - - // orders not executed; no position - await ensureNoPosition(userA) - await ensureNoPosition(userB) - }) - - it('keeper cannot execute order when conditions not met', async () => { - const [, canExecute] = await manager.checkOrder(market.address, userA.address, 501) - expect(canExecute).to.be.false - - await expect( - manager.connect(keeper).executeOrder(market.address, userA.address, 501), - ).to.be.revertedWithCustomError(manager, 'ManagerCannotExecuteError') - }) - - it('keeper can execute orders', async () => { - // commit a price which should make all orders executable - await commitPrice(parse6decimal('2800')) - - // execute two maker orders and the long order - await executeOrder(userA, 501) - await commitPrice() - await executeOrder(userA, 502) - await commitPrice() - await executeOrder(userB, 501) - await commitPrice() - - // validate positions - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('100')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) - await market.connect(userA).settle(userA.address, TX_OVERRIDES) - - await checkCompensation(3) - }) - - it('user can place an order using a signed message', async () => { - const orderId = await placeOrderWithSignature( - userA, - Side.MAKER, - Compare.GTE, - parse6decimal('1000'), - parse6decimal('-10'), - ) - expect(orderId).to.equal(BigNumber.from(503)) - await checkCompensation(0) - - await executeOrder(userA, 503) - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) - await commitPrice(parse6decimal('2801')) - }) - - it('user can cancel an order', async () => { - // user places an order - const orderId = await placeOrder(userA, Side.MAKER, Compare.GTE, parse6decimal('1001'), parse6decimal('-7')) - expect(orderId).to.equal(BigNumber.from(504)) - - // user cancels the order nonce - await expect(manager.connect(userA).cancelOrder(market.address, orderId, TX_OVERRIDES)) - .to.emit(manager, 'TriggerOrderCancelled') - .withArgs(market.address, userA.address, orderId) - - const storedOrder = await manager.orders(market.address, userA.address, orderId) - expect(storedOrder.isSpent).to.be.true - - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) - }) - - it('user can cancel an order using a signed message', async () => { - // user places an order - const orderId = await placeOrder(userA, Side.MAKER, Compare.GTE, parse6decimal('1002'), parse6decimal('-6')) - expect(orderId).to.equal(BigNumber.from(505)) - - // user creates and signs a message to cancel the order nonce - const message = { - ...createActionMessage(userA.address, orderId), - } - const signature = await signCancelOrderAction(userA, verifier, message) - - // keeper handles the request - await expect(manager.connect(keeper).cancelOrderWithSignature(message, signature, TX_OVERRIDES)) - .to.emit(manager, 'TriggerOrderCancelled') - .withArgs(market.address, userA.address, orderId) - await checkCompensation(0) - - const storedOrder = await manager.orders(market.address, userA.address, orderId) - expect(storedOrder.isSpent).to.be.true - - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) - }) - - it('non-delegated signer cannot interact', async () => { - // userB signs a message to change userA's position - advanceOrderId(userA) - const message: PlaceOrderActionStruct = { - order: { - ...DEFAULT_TRIGGER_ORDER, - side: Side.MAKER, - comparison: Compare.GTE, - price: parse6decimal('1003'), - delta: parse6decimal('2'), - }, - ...createActionMessage(userA.address, nextMessageNonce(), userB.address), - } - const signature = await signPlaceOrderAction(userB, verifier, message) - - await expect( - manager.connect(keeper).placeOrderWithSignature(message, signature, TX_OVERRIDES), - ).to.be.revertedWithCustomError(verifier, 'VerifierInvalidSignerError') - }) - - it('delegated signer can interact', async () => { - // userA delegates userB - await marketFactory.connect(userA).updateSigner(userB.address, true, TX_OVERRIDES) - - // userB signs a message to change userA's position - advanceOrderId(userA) - const message: PlaceOrderActionStruct = { - order: { - ...DEFAULT_TRIGGER_ORDER, - side: Side.MAKER, - comparison: Compare.GTE, - price: parse6decimal('1004'), - delta: parse6decimal('3'), - }, - ...createActionMessage(userA.address, nextMessageNonce(), userB.address), - } - const signature = await signPlaceOrderAction(userB, verifier, message) - - await expect(manager.connect(keeper).placeOrderWithSignature(message, signature, TX_OVERRIDES)) - .to.emit(manager, 'TriggerOrderPlaced') - .withArgs(market.address, userA.address, message.order, message.action.orderId) - - const storedOrder = await manager.orders(market.address, userA.address, message.action.orderId) - compareOrders(storedOrder, message.order) - - // order was not executed, so no change in position - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('90')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) - await checkCompensation(0) - }) - - it('charges flat interface fee upon execution', async () => { - const positionBefore = await getPendingPosition(userA, Side.MAKER) - const interfaceBalanceBefore = await dsu.balanceOf(userC.address) - - // user A reduces their maker position through a GUI which charges an interface fee - const feeAmount = parse6decimal('3.5') - const interfaceFee = { - interfaceFee: { - amount: feeAmount, - receiver: userC.address, - fixedFee: true, - unwrap: false, - }, - } - const positionDelta = parse6decimal('-5') - const orderId = await placeOrder( - userA, - Side.MAKER, - Compare.LTE, - parse6decimal('2828.28'), - positionDelta, - MAX_FEE, - constants.AddressZero, - interfaceFee, - ) - expect(orderId).to.equal(BigNumber.from(508)) - - // keeper executes the order and user settles themselves - await executeOrder(userA, orderId) - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(positionBefore.add(positionDelta)) - await commitPrice() - await market.connect(userA).settle(userA.address, TX_OVERRIDES) - - // validate positions - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('85')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.5')) - - // ensure fees were paid - await manager.connect(userC).claim(userC.address, false, TX_OVERRIDES) - expect(await dsu.balanceOf(userC.address)).to.equal(interfaceBalanceBefore.add(feeAmount.mul(1e12))) - await checkCompensation(1) - }) - - it('unwraps flat interface fee upon execution', async () => { - // user B increases their long position through a GUI which charges an interface fee - const feeAmount = parse6decimal('2.75') - const interfaceFee = { - interfaceFee: { - amount: feeAmount, - receiver: userC.address, - fixedFee: true, - unwrap: true, - }, - } - const positionDelta = parse6decimal('1.5') - const orderId = await placeOrder( - userB, - Side.LONG, - Compare.GTE, - parse6decimal('1900'), - positionDelta, - MAX_FEE, - constants.AddressZero, - interfaceFee, - ) - expect(orderId).to.equal(BigNumber.from(502)) - - // keeper executes the order and interface settles - await executeOrder(userB, orderId) - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('85')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('4')) // 2.5 + 1.5 - await commitPrice() - await market.connect(userC).settle(userB.address, TX_OVERRIDES) - - // ensure fees were paid - await manager.connect(userC).claim(userC.address, true, TX_OVERRIDES) - expect(await usdc.balanceOf(userC.address)).to.equal(feeAmount) - await checkCompensation(1) - }) - - it('unwraps notional interface fee upon execution', async () => { - const interfaceBalanceBefore = await usdc.balanceOf(userC.address) - - // userB increases their long position through a GUI which charges a notional interface fee - const interfaceFee = { - interfaceFee: { - amount: parse6decimal('0.0055'), - receiver: userC.address, - fixedFee: false, - unwrap: true, - }, - } - const orderId = await placeOrder( - userB, - Side.LONG, - Compare.GTE, - parse6decimal('0.01'), - parse6decimal('3'), - MAX_FEE, - constants.AddressZero, - interfaceFee, - ) - expect(orderId).to.equal(BigNumber.from(503)) - - // keeper executes the order and user settles - expect((await oracle.latest()).price).to.equal(parse6decimal('2801')) - // delta * price * fee amount = 3 * 2801 * 0.0055 - const expectedInterfaceFee = parse6decimal('46.2165') - await executeOrder(userB, orderId, expectedInterfaceFee) - expect(await getPendingPosition(userA, Side.MAKER)).to.equal(parse6decimal('85')) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('7')) // 4 + 3 - await commitPrice() - await market.connect(userB).settle(userB.address, TX_OVERRIDES) - - // ensure fees were paid - await manager.connect(userC).claim(userC.address, true, TX_OVERRIDES) - expect(await usdc.balanceOf(userC.address)).to.equal(interfaceBalanceBefore.add(expectedInterfaceFee)) - await checkCompensation(1) - }) - - it('users can close positions', async () => { - // can close directly - let orderId = await placeOrder(userA, Side.MAKER, Compare.GTE, constants.Zero, MAGIC_VALUE_CLOSE_POSITION) - expect(orderId).to.equal(BigNumber.from(509)) - - // can close using a signed message - orderId = await placeOrderWithSignature( - userB, - Side.LONG, - Compare.LTE, - parse6decimal('4000'), - MAGIC_VALUE_CLOSE_POSITION, - ) - expect(orderId).to.equal(BigNumber.from(504)) - - // keeper closes the taker position before removing liquidity - await executeOrder(userB, 504) - await commitPrice() - await executeOrder(userA, 509) - await commitPrice() +async function getFixture(): Promise { + const vars = loadFixture(fixture) + return vars +} - // settle and confirm positions are closed - await market.settle(userA.address, TX_OVERRIDES) - await ensureNoPosition(userA) - await market.settle(userB.address, TX_OVERRIDES) - await ensureNoPosition(userB) - }) +async function mockGasInfo() { + // Hardhat fork does not support Arbitrum built-ins; Kept produces "invalid opcode" error without this + const gasInfo = await smock.fake('ArbGasInfo', { + address: '0x000000000000000000000000000000000000006C', }) + // TODO: is this needed/useful? + // gasInfo.getL1BaseFeeEstimate.returns(0) +} - // tests interaction with markets; again userA has a maker position, userB has a long position, - // userC and userD interact only with trigger orders - describe('funded market', () => { - async function changePosition( - user: SignerWithAddress, - newMaker: BigNumberish = constants.MaxUint256, - newLong: BigNumberish = constants.MaxUint256, - newShort: BigNumberish = constants.MaxUint256, - ): Promise { - const tx = await market - .connect(user) - ['update(address,uint256,uint256,uint256,int256,bool)']( - user.address, - newMaker, - newLong, - newShort, - 0, - false, - TX_OVERRIDES, - ) - return (await getEventArguments(tx, 'OrderCreated')).order.timestamp - } - - before(async () => { - // ensure no positions were carried over from previous test suite - await ensureNoPosition(userA) - await ensureNoPosition(userB) - - await changePosition(userA, parse6decimal('10'), 0, 0) - await commitPrice(parse6decimal('2000')) - await market.settle(userA.address, TX_OVERRIDES) - - nextOrderId[userA.address] = BigNumber.from(600) - nextOrderId[userB.address] = BigNumber.from(600) - nextOrderId[userC.address] = BigNumber.from(600) - nextOrderId[userD.address] = BigNumber.from(600) - }) - - afterEach(async () => { - await checkCompensation(1) - }) - - it('can execute an order with pending position before oracle request fulfilled', async () => { - // userB has an unsettled long 1.2 position - await changePosition(userB, 0, parse6decimal('1.2'), 0) - expect((await market.positions(userB.address)).long).to.equal(0) - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('1.2')) - - // userB places an order to go long 0.8 more, and keeper executes it - const orderId = await placeOrder(userB, Side.LONG, Compare.LTE, parse6decimal('2013'), parse6decimal('0.8')) - expect(orderId).to.equal(BigNumber.from(601)) - advanceBlock() - const orderTimestamp = await executeOrder(userB, 601) - - // userB still has no settled position - expect((await market.positions(userB.address)).long).to.equal(0) - // but the order should increase their pending position to 2 - expect(await getPendingPosition(userB, Side.LONG)).to.equal(parse6decimal('2.0')) - - // commit price for both their 1.2 position and the 0.8 added through trigger order - await commitPrice(parse6decimal('2001.01'), await keeperOracle.next()) - await commitPrice(parse6decimal('2001.02'), orderTimestamp) - // settle userB and check position - await market.settle(userB.address, TX_OVERRIDES) - expect((await market.positions(userB.address)).long).to.equal(parse6decimal('2.0')) - }) - - it('can execute an order with pending position after oracle request fulfilled', async () => { - // userC has an unsettled short 0.3 position - await changePosition(userC, 0, 0, parse6decimal('1.3')) - expect((await market.positions(userC.address)).short).to.equal(0) - expect(await getPendingPosition(userC, Side.SHORT)).to.equal(parse6decimal('1.3')) - - // userC places an order to go short 1.2 more, and keeper executes it - const orderId = await placeOrder(userC, Side.SHORT, Compare.GTE, parse6decimal('1999.97'), parse6decimal('1.2')) - expect(orderId).to.equal(BigNumber.from(601)) - advanceBlock() - const orderTimestamp = await executeOrder(userC, 601) - - // prices are committed for both versions - await commitPrice(parse6decimal('2002.03'), await keeperOracle.next()) - await commitPrice(parse6decimal('2002.04'), orderTimestamp) - - // userC still has no settled position - expect((await market.positions(userC.address)).long).to.equal(0) - // but the order should increase their short position to 2.5 - expect(await getPendingPosition(userC, Side.SHORT)).to.equal(parse6decimal('2.5')) - - // after settling userC, they should be short 2.5 - await market.settle(userC.address, TX_OVERRIDES) - expect((await market.positions(userC.address)).short).to.equal(parse6decimal('2.5')) - }) - - it('can execute an order once market conditions allow', async () => { - // userD places an order to go long 3 once price dips below 2000 - const triggerPrice = parse6decimal('2000') - const orderId = await placeOrder(userD, Side.LONG, Compare.LTE, triggerPrice, parse6decimal('3')) - expect(orderId).to.equal(BigNumber.from(601)) - advanceBlock() - - // the order is not yet executable - const [, canExecuteBefore] = await manager.checkOrder(market.address, userD.address, orderId) - expect(canExecuteBefore).to.be.false - - // time passes, other users interact with market - let positionA = (await market.positions(userA.address)).maker - let positionC = (await market.positions(userC.address)).short - let marketPrice = (await oracle.latest()).price - - while (marketPrice.gt(triggerPrice)) { - // two users change their position - positionA = positionA.add(parse6decimal('0.05')) - const timestampA = await changePosition(userA, positionA, 0, 0) - positionC = positionC.sub(parse6decimal('0.04')) - const timestampC = await changePosition(userC, 0, 0, positionC) - - // oracle versions fulfilled - marketPrice = marketPrice.sub(parse6decimal('0.35')) - await commitPrice(marketPrice, timestampA) - await commitPrice(marketPrice, timestampC) - - // advance 5 minutes - await increase(60 * 5) - advanceBlock() - await setNextBlockBaseFee() - - // userA settled each time - await market.settle(userA.address, TX_OVERRIDES) - } - // userC settled after considerable time - await market.settle(userC.address, TX_OVERRIDES) - - // confirm order is now executable - const [, canExecuteAfter] = await manager.checkOrder(market.address, userD.address, orderId) - expect(canExecuteAfter).to.be.true - - // execute order - const orderTimestamp = await executeOrder(userD, 601) - expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('3')) - - // fulfill oracle version and settle - await commitPrice(parse6decimal('2000.1'), orderTimestamp) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userC.address)).short).to.equal(parse6decimal('2.26')) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('3')) - }) - - it('market reverts when attempting to close an unsettled positive position', async () => { - // userD submits order extending their long position, which keeper executes - let orderId = await placeOrder( - userD, - Side.LONG, - Compare.GTE, - parse6decimal('0.01'), - parse6decimal('1.5'), - MAX_FEE, - constants.AddressZero, - NO_INTERFACE_FEE, - ) - expect(orderId).to.equal(BigNumber.from(602)) - const longOrderTimestamp = await executeOrder(userD, orderId) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('3')) - expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('4.5')) - - // before settling, userD closes their long position - orderId = await placeOrder(userD, Side.LONG, Compare.LTE, parse6decimal('9999'), MAGIC_VALUE_CLOSE_POSITION) - expect(orderId).to.equal(BigNumber.from(603)) - - await expect( - manager.connect(keeper).executeOrder(market.address, userD.address, orderId, TX_OVERRIDES), - ).to.be.revertedWithCustomError(market, 'MarketOverCloseError') - - // keeper commits price, settles the long order - await commitPrice(parse6decimal('2000.2'), longOrderTimestamp) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('4.5')) - }) - - it('market handles attempt to close an unsettled negative position', async () => { - // userD submits order reducing their long position, which keeper executes - let orderId = await placeOrder( - userD, - Side.LONG, - Compare.GTE, - parse6decimal('0.01'), - parse6decimal('-0.5'), - MAX_FEE, - constants.AddressZero, - NO_INTERFACE_FEE, - ) - expect(orderId).to.equal(BigNumber.from(604)) - const reduceOrderTimestamp = await executeOrder(userD, orderId) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('4.5')) - expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('4')) - - // before settling, userD attempts to close their long position - orderId = await placeOrder(userD, Side.LONG, Compare.LTE, parse6decimal('9999'), MAGIC_VALUE_CLOSE_POSITION) - expect(orderId).to.equal(BigNumber.from(605)) - const closeOrderTimestamp = await executeOrder(userD, orderId) - - // keeper commits price, settles the long order - await commitPrice(parse6decimal('2000.31'), reduceOrderTimestamp) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('4')) - - // keeper commits another price, settles the close order - await commitPrice(parse6decimal('2000.32'), closeOrderTimestamp) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('0')) - }) - - it('charges notional interface fee on whole position when closing', async () => { - const interfaceBalanceBefore = await dsu.balanceOf(userB.address) - - // userD starts with a long 3 position - await changePosition(userD, 0, parse6decimal('3'), 0) - await commitPrice(parse6decimal('2000.4')) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('3')) - expect(await getPendingPosition(userD, Side.LONG)).to.equal(parse6decimal('3')) - - // userD closes their long position - const interfaceFee = { - interfaceFee: { - amount: parse6decimal('0.00654'), - receiver: userB.address, - fixedFee: false, - unwrap: false, - }, - } - const orderId = await placeOrder( - userD, - Side.LONG, - Compare.LTE, - parse6decimal('9999'), - MAGIC_VALUE_CLOSE_POSITION, - MAX_FEE, - constants.AddressZero, - interfaceFee, - ) - expect(orderId).to.equal(BigNumber.from(606)) - - const expectedInterfaceFee = parse6decimal('39.247848') // position * price * fee - const closeOrderTimestamp = await executeOrder(userD, orderId, expectedInterfaceFee) - expect(await getPendingPosition(userD, Side.LONG)).to.equal(constants.Zero) - - // ensure fees were paid - await manager.connect(userB).claim(userB.address, false, TX_OVERRIDES) - expect(await dsu.balanceOf(userB.address)).to.equal(interfaceBalanceBefore.add(expectedInterfaceFee.mul(1e12))) - - // settle before next test - await commitPrice(parse6decimal('2000.4'), closeOrderTimestamp) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('0')) - }) - - it('charges notional interface fee when closing with a pending negative position', async () => { - const interfaceBalanceBefore = await dsu.balanceOf(userB.address) - - // userD starts with a short 2 position - await changePosition(userD, 0, 0, parse6decimal('2')) - await commitPrice(parse6decimal('2000.5')) - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).short).to.equal(parse6decimal('2')) - - // userD reduces their position by 0.35 but does not settle - const negOrderTimestamp = await changePosition(userD, 0, 0, parse6decimal('1.65')) - expect(await getPendingPosition(userD, Side.SHORT)).to.equal(parse6decimal('1.65')) - - // userD closes their short position - const interfaceFee = { - interfaceFee: { - amount: parse6decimal('0.0051'), - receiver: userB.address, - fixedFee: false, - unwrap: false, - }, - } - const orderId = await placeOrder( - userD, - Side.SHORT, - Compare.LTE, - parse6decimal('9999'), - MAGIC_VALUE_CLOSE_POSITION, - MAX_FEE, - constants.AddressZero, - interfaceFee, - ) - expect(orderId).to.equal(BigNumber.from(607)) - - // position * price * fee = 1.65 * 2000.5 * 0.0051 - const expectedInterfaceFee = parse6decimal('16.8342075') - await setNextBlockBaseFee() - const closeOrderTimestamp = await executeOrder(userD, orderId, expectedInterfaceFee) - expect(await getPendingPosition(userD, Side.SHORT)).to.equal(constants.Zero) - - // ensure fees were paid - await manager.connect(userB).claim(userB.address, false, TX_OVERRIDES) - expect(await dsu.balanceOf(userB.address)).to.equal(interfaceBalanceBefore.add(expectedInterfaceFee.mul(1e12))) - - // settle before next test - await commitPrice(parse6decimal('2000.4'), negOrderTimestamp) - await commitPrice(parse6decimal('2000.4'), closeOrderTimestamp) - await setNextBlockBaseFee() - await market.settle(userD.address, TX_OVERRIDES) - expect((await market.positions(userD.address)).long).to.equal(parse6decimal('0')) - }) - }) -}) +if (process.env.FORK_NETWORK === 'arbitrum') RunManagerTests('Manager_Arbitrum', getFixture, mockGasInfo) From dde610f3c495a578a15b61c9d9c2e4ab757d1cbc Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 16:09:41 -0400 Subject: [PATCH 13/23] optimism manager implementation and tests --- .../contracts/Manager_Optimism.sol | 38 ++++ packages/perennial-order/package.json | 1 + .../test/helpers/setupHelpers.ts | 160 ++++++++++++++++- .../test/integration/Manager.test.ts | 4 +- .../test/integration/Manager_Arbitrum.test.ts | 161 +---------------- .../test/integration/Manager_Optimism.test.ts | 168 ++++++++++++++++++ 6 files changed, 373 insertions(+), 159 deletions(-) create mode 100644 packages/perennial-order/contracts/Manager_Optimism.sol create mode 100644 packages/perennial-order/test/integration/Manager_Optimism.test.ts diff --git a/packages/perennial-order/contracts/Manager_Optimism.sol b/packages/perennial-order/contracts/Manager_Optimism.sol new file mode 100644 index 000000000..45b7e1807 --- /dev/null +++ b/packages/perennial-order/contracts/Manager_Optimism.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { IEmptySetReserve } from "@equilibria/emptyset-batcher/interfaces/IEmptySetReserve.sol"; +import { Kept, Kept_Optimism, Token18, UFixed18 } from "@equilibria/root/attribute/Kept/Kept_Optimism.sol"; +import { Token6 } from "@equilibria/root/token/types/Token6.sol"; +import { IMarketFactory } from "@equilibria/perennial-v2/contracts/interfaces/IMarketFactory.sol"; + +import { IOrderVerifier, Manager } from "./Manager.sol"; + +contract Manager_Optimism is Manager, Kept_Optimism { + /// @dev passthrough constructor + constructor( + Token6 usdc, + Token18 dsu, + IEmptySetReserve reserve, + IMarketFactory marketFactory, + IOrderVerifier verifier + ) + Manager(usdc, dsu, reserve, marketFactory, verifier) {} + + /// @dev Use the Kept_Optimism implementation for calculating the dynamic fee + function _calldataFee( + bytes memory applicableCalldata, + UFixed18 multiplierCalldata, + uint256 bufferCalldata + ) internal view override(Kept_Optimism, Kept) returns (UFixed18) { + return Kept_Optimism._calldataFee(applicableCalldata, multiplierCalldata, bufferCalldata); + } + + /// @dev Use the base implementation for raising the keeper fee + function _raiseKeeperFee( + UFixed18 amount, + bytes memory data + ) internal override(Manager, Kept) returns (UFixed18) { + return Manager._raiseKeeperFee(amount, data); + } +} diff --git a/packages/perennial-order/package.json b/packages/perennial-order/package.json index 59e784688..f5ead3fff 100644 --- a/packages/perennial-order/package.json +++ b/packages/perennial-order/package.json @@ -13,6 +13,7 @@ "gasReport": "REPORT_GAS=true OPTIMIZER_ENABLED=true yarn test:integration", "test": "hardhat test test/unit/*", "test:integration": "FORK_ENABLED=true FORK_NETWORK=arbitrum FORK_BLOCK_NUMBER=243648015 hardhat test test/integration/*", + "test:integrationBase": "FORK_ENABLED=true FORK_NETWORK=base FORK_BLOCK_NUMBER=21067741 hardhat test test/integration/*", "coverage": "hardhat coverage --testfiles 'test/unit/*'", "coverage:integration": "FORK_ENABLED=true FORK_NETWORK=arbitrum FORK_BLOCK_NUMBER=243648015 hardhat coverage --testfiles 'test/integration/*'", "lint": "eslint --fix --ext '.ts,.js' ./ && solhint 'contracts/**/*.sol' --fix", diff --git a/packages/perennial-order/test/helpers/setupHelpers.ts b/packages/perennial-order/test/helpers/setupHelpers.ts index 48d5e2844..67b463ab8 100644 --- a/packages/perennial-order/test/helpers/setupHelpers.ts +++ b/packages/perennial-order/test/helpers/setupHelpers.ts @@ -1,22 +1,41 @@ -import { BigNumber, CallOverrides } from 'ethers' -import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' +import { CallOverrides, utils } from 'ethers' +import { Address } from 'hardhat-deploy/dist/types' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +import { IVerifier, MarketFactory, MarketFactory__factory } from '@equilibria/perennial-v2/types/generated' +import { + IKeeperOracle, + IOracleFactory, + KeeperOracle__factory, + OracleFactory, + PythFactory, + PythFactory__factory, +} from '@equilibria/perennial-v2-oracle/types/generated' import { + GasOracle__factory, IEmptySetReserve, IERC20Metadata, + IERC20Metadata__factory, + IManager, IMarket, IMarketFactory, IOracleProvider, IOrderVerifier, - Manager_Arbitrum, } from '../../types/generated' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +import { createMarket, deployMarketImplementation } from './marketHelpers' +import { createPythOracle, deployOracleFactory } from './oracleHelpers' +import { parse6decimal } from '../../../common/testutil/types' +import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' + +const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' export interface FixtureVars { dsu: IERC20Metadata usdc: IERC20Metadata reserve: IEmptySetReserve keeperOracle: IKeeperOracle - manager: Manager_Arbitrum + manager: IManager marketFactory: IMarketFactory market: IMarket oracle: IOracleProvider @@ -29,3 +48,134 @@ export interface FixtureVars { keeper: SignerWithAddress oracleFeeReceiver: SignerWithAddress } + +// creates an ETH market using a locally deployed factory and oracle +export async function createMarketETH( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythOracleFactory: PythFactory, + marketFactory: IMarketFactory, + dsu: IERC20Metadata, + overrides?: CallOverrides, +): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { + // Create oracles needed to support the market + const [keeperOracle, oracle] = await createPythOracle( + owner, + oracleFactory, + pythOracleFactory, + PYTH_ETH_USD_PRICE_FEED, + 'ETH-USD', + overrides, + ) + // Create the market in which user or collateral account may interact + const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) + await keeperOracle.register(oracle.address) + await oracle.register(market.address) + return [market, oracle, keeperOracle] +} + +// Deploys the market factory and configures default protocol parameters +export async function deployMarketFactory( + owner: SignerWithAddress, + pauser: SignerWithAddress, + oracleFactoryAddress: Address, + verifierAddress: Address, + marketImplAddress: Address, +): Promise { + const marketFactory = await new MarketFactory__factory(owner).deploy( + oracleFactoryAddress, + verifierAddress, + marketImplAddress, + ) + await marketFactory.connect(owner).initialize() + + // Set protocol parameters + await marketFactory.updatePauser(pauser.address) + await marketFactory.updateParameter({ + maxFee: parse6decimal('0.01'), + maxLiquidationFee: parse6decimal('20'), + maxCut: parse6decimal('0.50'), + maxRate: parse6decimal('10.00'), + minMaintenance: parse6decimal('0.01'), + minEfficiency: parse6decimal('0.1'), + referralFee: 0, + minScale: parse6decimal('0.001'), + maxStaleAfter: 7200, + }) + + return marketFactory +} + +// Deploys OracleFactory and then MarketFactory +export async function deployProtocol( + owner: SignerWithAddress, + dsuAddress: Address, +): Promise<[IMarketFactory, IERC20Metadata, IOracleFactory]> { + // Deploy the oracle factory, which markets created by the market factory will query + const dsu = IERC20Metadata__factory.connect(dsuAddress, owner) + const oracleFactory = await deployOracleFactory(owner) + + // Deploy the market factory and authorize it with the oracle factory + const marketVerifier = await new Verifier__factory(owner).deploy() + const marketFactory = await deployProtocolForOracle(owner, oracleFactory, marketVerifier) + return [marketFactory, dsu, oracleFactory] +} + +// Deploys a market implementation and the MarketFactory for a provided oracle factory +async function deployProtocolForOracle( + owner: SignerWithAddress, + oracleFactory: OracleFactory, + verifier: IVerifier, +): Promise { + // Deploy protocol contracts + const marketImpl = await deployMarketImplementation(owner, verifier.address) + const marketFactory = await deployMarketFactory( + owner, + owner, + oracleFactory.address, + verifier.address, + marketImpl.address, + ) + return marketFactory +} + +export async function deployPythOracleFactory( + owner: SignerWithAddress, + oracleFactory: IOracleFactory, + pythAddress: Address, + chainlinkFeedAddress: Address, +): Promise { + const commitmentGasOracle = await new GasOracle__factory(owner).deploy( + chainlinkFeedAddress, + 8, + 1_000_000, + utils.parseEther('1.02'), + 1_000_000, + 0, + 0, + 0, + ) + const settlementGasOracle = await new GasOracle__factory(owner).deploy( + chainlinkFeedAddress, + 8, + 200_000, + utils.parseEther('1.02'), + 500_000, + 0, + 0, + 0, + ) + + // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices + const keeperOracleImpl = await new KeeperOracle__factory(owner).deploy(60) + const pythOracleFactory = await new PythFactory__factory(owner).deploy( + pythAddress, + commitmentGasOracle.address, + settlementGasOracle.address, + keeperOracleImpl.address, + ) + await pythOracleFactory.initialize(oracleFactory.address) + await pythOracleFactory.updateParameter(1, 0, 4, 10) + await oracleFactory.register(pythOracleFactory.address) + return pythOracleFactory +} diff --git a/packages/perennial-order/test/integration/Manager.test.ts b/packages/perennial-order/test/integration/Manager.test.ts index 0f8d283f2..1542bc247 100644 --- a/packages/perennial-order/test/integration/Manager.test.ts +++ b/packages/perennial-order/test/integration/Manager.test.ts @@ -10,7 +10,7 @@ import { parse6decimal } from '../../../common/testutil/types' import { IERC20Metadata, IMarketFactory, IMarket, IOracleProvider } from '@equilibria/perennial-v2/types/generated' import { IKeeperOracle } from '@equilibria/perennial-v2-oracle/types/generated' -import { IEmptySetReserve, IOrderVerifier, Manager_Arbitrum } from '../../types/generated' +import { IEmptySetReserve, IManager, IOrderVerifier } from '../../types/generated' import { signAction, signCancelOrderAction, signPlaceOrderAction } from '../helpers/eip712' import { @@ -51,7 +51,7 @@ export function RunManagerTests( let usdc: IERC20Metadata let reserve: IEmptySetReserve let keeperOracle: IKeeperOracle - let manager: Manager_Arbitrum + let manager: IManager let marketFactory: IMarketFactory let market: IMarket let oracle: IOracleProvider diff --git a/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts b/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts index 3b0b2c2c9..1a7cf8dc5 100644 --- a/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts +++ b/packages/perennial-order/test/integration/Manager_Arbitrum.test.ts @@ -1,39 +1,26 @@ import { expect } from 'chai' -import { BigNumber, CallOverrides, constants, utils } from 'ethers' +import { BigNumber, CallOverrides, utils } from 'ethers' import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { smock } from '@defi-wonderland/smock' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import HRE from 'hardhat' -import { IMarket, MarketFactory, MarketFactory__factory } from '@equilibria/perennial-v2/types/generated' -import { - IKeeperOracle, - IOracleFactory, - IOracleProvider, - KeeperOracle__factory, - OracleFactory, - PythFactory, - PythFactory__factory, - GasOracle__factory, -} from '@equilibria/perennial-v2-oracle/types/generated' -import { Verifier__factory } from '@equilibria/perennial-v2-verifier/types/generated' -import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { ArbGasInfo, IEmptySetReserve__factory, IERC20Metadata, IERC20Metadata__factory, IManager, + IMarket, IMarketFactory, - IVerifier, Manager_Arbitrum__factory, OrderVerifier__factory, } from '../../types/generated' import { impersonate } from '../../../common/testutil' -import { Address } from 'hardhat-deploy/dist/types' + import { parse6decimal } from '../../../common/testutil/types' -import { createPythOracle, deployOracleFactory } from '../helpers/oracleHelpers' -import { createMarket, deployMarketImplementation, transferCollateral } from '../helpers/marketHelpers' -import { FixtureVars } from '../helpers/setupHelpers' +import { transferCollateral } from '../helpers/marketHelpers' +import { createMarketETH, deployProtocol, deployPythOracleFactory, FixtureVars } from '../helpers/setupHelpers' import { RunManagerTests } from './Manager.test' const { ethers } = HRE @@ -44,139 +31,9 @@ const DSU_RESERVE = '0x0d49c416103Cbd276d9c3cd96710dB264e3A0c27' const USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' // Arbitrum native USDC, a 6-decimal token const PYTH_ADDRESS = '0xff1a0f4744e8582DF1aE09D5611b887B6a12925C' -const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' const CHAINLINK_ETH_USD_FEED = '0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612' // price feed used for keeper compensation -// creates an ETH market using a locally deployed factory and oracle -export async function createMarketETH( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, - pythOracleFactory: PythFactory, - marketFactory: IMarketFactory, - dsu: IERC20Metadata, - overrides?: CallOverrides, -): Promise<[IMarket, IOracleProvider, IKeeperOracle]> { - // Create oracles needed to support the market - const [keeperOracle, oracle] = await createPythOracle( - owner, - oracleFactory, - pythOracleFactory, - PYTH_ETH_USD_PRICE_FEED, - 'ETH-USD', - overrides, - ) - // Create the market in which user or collateral account may interact - const market = await createMarket(owner, marketFactory, dsu, oracle, undefined, undefined, overrides ?? {}) - await keeperOracle.register(oracle.address) - await oracle.register(market.address) - return [market, oracle, keeperOracle] -} - -// Deploys the market factory and configures default protocol parameters -async function deployMarketFactory( - owner: SignerWithAddress, - pauser: SignerWithAddress, - oracleFactoryAddress: Address, - verifierAddress: Address, - marketImplAddress: Address, -): Promise { - const marketFactory = await new MarketFactory__factory(owner).deploy( - oracleFactoryAddress, - verifierAddress, - marketImplAddress, - ) - await marketFactory.connect(owner).initialize() - - // Set protocol parameters - await marketFactory.updatePauser(pauser.address) - await marketFactory.updateParameter({ - maxFee: parse6decimal('0.01'), - maxLiquidationFee: parse6decimal('20'), - maxCut: parse6decimal('0.50'), - maxRate: parse6decimal('10.00'), - minMaintenance: parse6decimal('0.01'), - minEfficiency: parse6decimal('0.1'), - referralFee: 0, - minScale: parse6decimal('0.001'), - maxStaleAfter: 7200, - }) - - return marketFactory -} - -// Deploys OracleFactory and then MarketFactory -export async function deployProtocol( - owner: SignerWithAddress, -): Promise<[IMarketFactory, IERC20Metadata, IOracleFactory]> { - // Deploy the oracle factory, which markets created by the market factory will query - const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, owner) - const oracleFactory = await deployOracleFactory(owner) - - // Deploy the market factory and authorize it with the oracle factory - const marketVerifier = await new Verifier__factory(owner).deploy() - const marketFactory = await deployProtocolForOracle(owner, oracleFactory, marketVerifier) - return [marketFactory, dsu, oracleFactory] -} - -// Deploys a market implementation and the MarketFactory for a provided oracle factory -async function deployProtocolForOracle( - owner: SignerWithAddress, - oracleFactory: OracleFactory, - verifier: IVerifier, -): Promise { - // Deploy protocol contracts - const marketImpl = await deployMarketImplementation(owner, verifier.address) - const marketFactory = await deployMarketFactory( - owner, - owner, - oracleFactory.address, - verifier.address, - marketImpl.address, - ) - return marketFactory -} - -export async function deployPythOracleFactory( - owner: SignerWithAddress, - oracleFactory: IOracleFactory, -): Promise { - const commitmentGasOracle = await new GasOracle__factory(owner).deploy( - CHAINLINK_ETH_USD_FEED, - 8, - 1_000_000, - utils.parseEther('1.02'), - 1_000_000, - 0, - 0, - 0, - ) - const settlementGasOracle = await new GasOracle__factory(owner).deploy( - CHAINLINK_ETH_USD_FEED, - 8, - 200_000, - utils.parseEther('1.02'), - 500_000, - 0, - 0, - 0, - ) - - // Deploy a Pyth keeper oracle factory, which we'll need to meddle with prices - const keeperOracleImpl = await new KeeperOracle__factory(owner).deploy(60) - const pythOracleFactory = await new PythFactory__factory(owner).deploy( - PYTH_ADDRESS, - commitmentGasOracle.address, - settlementGasOracle.address, - keeperOracleImpl.address, - ) - await pythOracleFactory.initialize(oracleFactory.address) - await pythOracleFactory.updateParameter(1, 0, 4, 10) - await oracleFactory.register(pythOracleFactory.address) - return pythOracleFactory -} - -// TODO: consider rolling this into setupUser export async function fundWalletDSU( wallet: SignerWithAddress, amount: BigNumber, @@ -210,10 +67,10 @@ async function setupUser( const fixture = async (): Promise => { // deploy the protocol and create a market const [owner, userA, userB, userC, userD, keeper, oracleFeeReceiver] = await ethers.getSigners() - const [marketFactory, dsu, oracleFactory] = await deployProtocol(owner) + const [marketFactory, dsu, oracleFactory] = await deployProtocol(owner, DSU_ADDRESS) const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) const reserve = IEmptySetReserve__factory.connect(DSU_RESERVE, owner) - const pythOracleFactory = await deployPythOracleFactory(owner, oracleFactory) + const pythOracleFactory = await deployPythOracleFactory(owner, oracleFactory, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) const [market, oracle, keeperOracle] = await createMarketETH( owner, oracleFactory, @@ -282,7 +139,7 @@ async function getFixture(): Promise { async function mockGasInfo() { // Hardhat fork does not support Arbitrum built-ins; Kept produces "invalid opcode" error without this - const gasInfo = await smock.fake('ArbGasInfo', { + /*const gasInfo = */ await smock.fake('ArbGasInfo', { address: '0x000000000000000000000000000000000000006C', }) // TODO: is this needed/useful? diff --git a/packages/perennial-order/test/integration/Manager_Optimism.test.ts b/packages/perennial-order/test/integration/Manager_Optimism.test.ts new file mode 100644 index 000000000..c7679eee6 --- /dev/null +++ b/packages/perennial-order/test/integration/Manager_Optimism.test.ts @@ -0,0 +1,168 @@ +import { expect } from 'chai' +import { BigNumber, CallOverrides, utils } from 'ethers' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { smock } from '@defi-wonderland/smock' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import HRE from 'hardhat' + +import { + IEmptySetReserve__factory, + IERC20Metadata, + IERC20Metadata__factory, + IManager, + IMarket, + IMarketFactory, + Manager_Optimism__factory, + OptGasInfo, + OrderVerifier__factory, +} from '../../types/generated' +import { impersonate } from '../../../common/testutil' + +import { parse6decimal } from '../../../common/testutil/types' +import { transferCollateral } from '../helpers/marketHelpers' +import { createMarketETH, deployProtocol, deployPythOracleFactory, FixtureVars } from '../helpers/setupHelpers' +import { RunManagerTests } from './Manager.test' + +const { ethers } = HRE + +const DSU_ADDRESS = '0x7b4Adf64B0d60fF97D672E473420203D52562A84' // Digital Standard Unit, an 18-decimal token +const DSU_RESERVE = '0x5FA881826AD000D010977645450292701bc2f56D' +const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC, a 6-decimal token, used by DSU reserve above +const USDC_HOLDER = '0xF977814e90dA44bFA03b6295A0616a897441aceC' // EOA has 302mm USDC at height 21067741 + +const PYTH_ADDRESS = '0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a' + +const CHAINLINK_ETH_USD_FEED = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70' + +export async function fundWalletDSU( + wallet: SignerWithAddress, + amount: BigNumber, + overrides?: CallOverrides, +): Promise { + const dsu = IERC20Metadata__factory.connect(DSU_ADDRESS, wallet) + const reserve = IEmptySetReserve__factory.connect(DSU_RESERVE, wallet) + const balanceBefore = await dsu.balanceOf(wallet.address) + + // fund wallet with USDC and then mint using reserve + await fundWalletUSDC(wallet, amount.div(1e12), overrides) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, wallet) + await usdc.connect(wallet).approve(reserve.address, amount.div(1e12)) + await reserve.mint(amount) + + expect((await dsu.balanceOf(wallet.address)).sub(balanceBefore)).to.equal(amount) +} + +async function fundWalletUSDC( + wallet: SignerWithAddress, + amount: BigNumber, + overrides?: CallOverrides, +): Promise { + const usdcOwner = await impersonate.impersonateWithBalance(USDC_HOLDER, utils.parseEther('10')) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, usdcOwner) + + expect(await usdc.balanceOf(USDC_HOLDER)).to.be.greaterThan(amount) + await usdc.transfer(wallet.address, amount, overrides ?? {}) +} + +// prepares an account for use with the market and manager +async function setupUser( + dsu: IERC20Metadata, + marketFactory: IMarketFactory, + market: IMarket, + manager: IManager, + user: SignerWithAddress, + amount: BigNumber, +) { + // funds, approves, and deposits DSU into the market + await fundWalletDSU(user, amount.mul(1e12)) + await dsu.connect(user).approve(market.address, amount.mul(1e12)) + await transferCollateral(user, market, amount) + + // allows manager to interact with markets on the user's behalf + await marketFactory.connect(user).updateOperator(manager.address, true) +} + +const fixture = async (): Promise => { + // deploy the protocol and create a market + const [owner, userA, userB, userC, userD, keeper, oracleFeeReceiver] = await ethers.getSigners() + const [marketFactory, dsu, oracleFactory] = await deployProtocol(owner, DSU_ADDRESS) + const usdc = IERC20Metadata__factory.connect(USDC_ADDRESS, owner) + const reserve = IEmptySetReserve__factory.connect(DSU_RESERVE, owner) + const pythOracleFactory = await deployPythOracleFactory(owner, oracleFactory, PYTH_ADDRESS, CHAINLINK_ETH_USD_FEED) + const [market, oracle, keeperOracle] = await createMarketETH( + owner, + oracleFactory, + pythOracleFactory, + marketFactory, + dsu, + ) + + // deploy the order manager + const verifier = await new OrderVerifier__factory(owner).deploy(marketFactory.address) + const manager = await new Manager_Optimism__factory(owner).deploy( + USDC_ADDRESS, + dsu.address, + DSU_RESERVE, + marketFactory.address, + verifier.address, + ) + + const keepConfig = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 1_000_000, // buffer for withdrawing keeper fee from market + multiplierCalldata: 0, + bufferCalldata: 0, + } + const keepConfigBuffered = { + multiplierBase: ethers.utils.parseEther('1'), + bufferBase: 1_000_000, // for price commitment + multiplierCalldata: ethers.utils.parseEther('1'), + bufferCalldata: 0, + } + await manager.initialize(CHAINLINK_ETH_USD_FEED, keepConfig, keepConfigBuffered) + + // TODO: can user setup be handled by the test in such a way that the test calls loadFixture + // after some nested setup? + // fund accounts and deposit all into market + const amount = parse6decimal('100000') + await setupUser(dsu, marketFactory, market, manager, userA, amount) + await setupUser(dsu, marketFactory, market, manager, userB, amount) + await setupUser(dsu, marketFactory, market, manager, userC, amount) + await setupUser(dsu, marketFactory, market, manager, userD, amount) + + return { + dsu, + usdc, + reserve, + keeperOracle, + manager, + marketFactory, + market, + oracle, + verifier, + owner, + userA, + userB, + userC, + userD, + keeper, + oracleFeeReceiver, + } +} + +async function getFixture(): Promise { + const vars = loadFixture(fixture) + return vars +} + +async function mockGasInfo() { + const gasInfo = await smock.fake('OptGasInfo', { + address: '0x420000000000000000000000000000000000000F', + }) + gasInfo.getL1GasUsed.returns(1600) + gasInfo.l1BaseFee.returns(18476655731) + gasInfo.baseFeeScalar.returns(2768304) + gasInfo.decimals.returns(6) +} + +if (process.env.FORK_NETWORK === 'base') RunManagerTests('Manager_Optimism', getFixture, mockGasInfo) From 465513602e0415cd6d49485d49072e7eef1a6ca8 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 16:18:53 -0400 Subject: [PATCH 14/23] first attempt to run base test in CI --- .github/workflows/CI.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9332f8d06..50e056c62 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -40,6 +40,7 @@ jobs: # [CORE] core-unit-test: + if: false name: '[Core] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -77,6 +78,7 @@ jobs: delete-old-comments: true if: ${{ github.event_name == 'pull_request' && env.PARSER_BROKEN != 'true' }} core-integration-test: + if: false name: '[Core] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -115,6 +117,7 @@ jobs: delete-old-comments: true if: ${{ github.event_name == 'pull_request' && env.PARSER_BROKEN != 'true' }} core-combined-test: + if: false name: '[Core] Combined Tests w/ Coverage' runs-on: ubuntu-latest needs: [core-unit-test, core-integration-test] @@ -194,7 +197,7 @@ jobs: run: yarn --frozen-lockfile - name: Compile run: yarn workspaces run compile # compile all packages - - name: Run tests + - name: Run tests with coverage on Arbitrum env: MOCHA_REPORTER: dot MOCHA_RETRY_COUNT: 2 @@ -206,9 +209,17 @@ jobs: with: name: account_integration_test_coverage path: ./packages/perennial-account/coverage/lcov.info + - name: Run tests on Base + env: + MOCHA_REPORTER: dot + MOCHA_RETRY_COUNT: 2 + BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} + run: | + yarn workspace @equilibria/perennial-v2-account run test:integration # [ORACLE] oracle-unit-test: + if: false name: '[Oracle] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -238,6 +249,7 @@ jobs: name: oracle_unit_test_coverage path: ./packages/perennial-oracle/coverage/lcov.info oracle-integration-test: + if: false name: '[Oracle] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -268,6 +280,7 @@ jobs: name: oracle_integration_test_coverage path: ./packages/perennial-oracle/coverage/lcov.info oracle-integrationSepolia-test: + if: false name: '[Oracle] Sepolia Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -361,6 +374,7 @@ jobs: # [VAULT] vault-unit-test: + if: false name: '[Vault] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -390,6 +404,7 @@ jobs: name: vault_unit_test_coverage path: ./packages/perennial-vault/coverage/lcov.info vault-integration-test: + if: false name: '[Vault] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -422,6 +437,7 @@ jobs: # [EXTENSIONS] extensions-unit-test: + if: false name: '[Extensions] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -451,6 +467,7 @@ jobs: name: extensions_unit_test_coverage path: ./packages/perennial-extensions/coverage/lcov.info extensions-integration-test: + if: false name: '[Extensions] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -483,6 +500,7 @@ jobs: # [VERIFIER] verifier-unit-test: + if: false name: '[Verifier] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: From 41df7542799c32992558cc19b3ac412ab9a212c3 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 17:48:27 -0400 Subject: [PATCH 15/23] post-rebase cleanup --- .github/workflows/CI.yml | 2 +- .../perennial-account/test/helpers/arbitrumHelpers.ts | 3 ++- packages/perennial-account/test/helpers/baseHelpers.ts | 2 +- .../perennial-account/test/helpers/setupHelpers.ts | 2 +- .../test/integration/Arbitrum.test.ts | 2 +- .../test/integration/Controller.test.ts | 10 ++++++++-- packages/perennial-order/test/helpers/marketHelpers.ts | 5 ++--- packages/perennial-order/test/helpers/oracleHelpers.ts | 4 +--- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 235c597d3..b0edb49e5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -215,7 +215,7 @@ jobs: MOCHA_RETRY_COUNT: 2 BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} run: | - yarn workspace @equilibria/perennial-v2-account run test:integration + yarn workspace @perennial/account run test:integration # [ORACLE] oracle-unit-test: diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index cda6cd980..827bc153f 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -1,11 +1,12 @@ import { expect } from 'chai' import { BigNumber, CallOverrides, constants, utils } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' +import { IOracleFactory, PythFactory } from '@perennial/oracle/types/generated' import { createFactories, deployController } from './setupHelpers' import { Account__factory, AccountVerifier__factory, + AggregatorV3Interface, Controller, Controller_Arbitrum, Controller_Arbitrum__factory, diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts index 22828fe89..5f83d2177 100644 --- a/packages/perennial-account/test/helpers/baseHelpers.ts +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { BigNumber, CallOverrides, constants, utils } from 'ethers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { IOracleFactory, PythFactory } from '@equilibria/perennial-v2-oracle/types/generated' +import { IOracleFactory, PythFactory } from '@perennial/oracle/types/generated' import { createFactories, deployController } from './setupHelpers' import { Account__factory, diff --git a/packages/perennial-account/test/helpers/setupHelpers.ts b/packages/perennial-account/test/helpers/setupHelpers.ts index d96bf0e7d..4f06bd749 100644 --- a/packages/perennial-account/test/helpers/setupHelpers.ts +++ b/packages/perennial-account/test/helpers/setupHelpers.ts @@ -55,9 +55,9 @@ import { Oracle, AggregatorV3Interface__factory, } from '@perennial/oracle/types/generated' -import { OracleVersionStruct } from '../../types/generated/@perennial/core/contracts/interfaces/IOracleProvider' import { Verifier__factory } from '@perennial/verifier/types/generated' import { expect } from 'chai' +import { OracleVersionStruct } from '@perennial/core/types/generated/contracts/interfaces/IOracleProvider' const PYTH_ETH_USD_PRICE_FEED = '0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace' const PYTH_BTC_USD_PRICE_FEED = '0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43' diff --git a/packages/perennial-account/test/integration/Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts index 94cf5664c..7f73886e2 100644 --- a/packages/perennial-account/test/integration/Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -21,7 +21,7 @@ import { RunIncentivizedTests } from './Controller_Incentivized.test' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { Controller_Incentivized, IMarketFactory } from '../../types/generated' import { RunAccountTests } from './Account.test' -import { AggregatorV3Interface } from '@equilibria/perennial-v2-oracle/types/generated' +import { AggregatorV3Interface } from '@perennial/oracle/types/generated' import { RunControllerBaseTests } from './Controller.test' const { ethers } = HRE diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index a7efcc225..06fdcf63f 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -8,8 +8,14 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' import { currentBlockTimestamp } from '../../../common/testutil/time' import { getEventArguments } from '../../../common/testutil/transaction' import { parse6decimal } from '../../../common/testutil/types' -import { Account, Account__factory, AccountVerifier__factory, Controller, IERC20Metadata } from '../../types/generated' -import { IAccountVerifier } from '../../types/generated/contracts/interfaces' +import { + Account, + Account__factory, + AccountVerifier__factory, + Controller, + IAccountVerifier, + IERC20Metadata, +} from '../../types/generated' import { IMarket, IMarketFactory } from '@perennial/core/types/generated' import { signDeployAccount, signMarketTransfer, signRebalanceConfigChange, signWithdrawal } from '../helpers/erc712' import { advanceToPrice, deployController, DeploymentVars } from '../helpers/setupHelpers' diff --git a/packages/perennial-order/test/helpers/marketHelpers.ts b/packages/perennial-order/test/helpers/marketHelpers.ts index 352cf3ecf..af15a32a5 100644 --- a/packages/perennial-order/test/helpers/marketHelpers.ts +++ b/packages/perennial-order/test/helpers/marketHelpers.ts @@ -7,21 +7,20 @@ import { IMarket, IMarketFactory, InvariantLib__factory, - IOracle, MarketParameterStorageLib__factory, - MarketParameterStruct, Market__factory, PositionStorageGlobalLib__factory, PositionStorageLocalLib__factory, RiskParameterStorageLib__factory, - RiskParameterStruct, VersionLib__factory, VersionStorageLib__factory, MagicValueLib__factory, } from '@perennial/core/types/generated' +import { IOracle } from '@perennial/oracle/types/generated' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { IERC20Metadata } from '../../types/generated' import { parse6decimal } from '../../../common/testutil/types' +import { MarketParameterStruct, RiskParameterStruct } from '@perennial/core/types/generated/contracts/Market' // TODO: consider sharing this across extensions, possibly by moving to packages/common // Using a provided factory, create a new market and set some reasonable initial parameters diff --git a/packages/perennial-order/test/helpers/oracleHelpers.ts b/packages/perennial-order/test/helpers/oracleHelpers.ts index ba3cafef1..99c8eb0ab 100644 --- a/packages/perennial-order/test/helpers/oracleHelpers.ts +++ b/packages/perennial-order/test/helpers/oracleHelpers.ts @@ -13,11 +13,9 @@ import { Oracle__factory, OracleFactory, OracleFactory__factory, - OracleVersionStruct, PythFactory, } from '@perennial/oracle/types/generated' - -const { ethers } = HRE +import { OracleVersionStruct } from '@perennial/oracle/types/generated/contracts/Oracle' // TODO: consider sharing this across extensions, possibly by moving to packages/common // Simulates an oracle update from KeeperOracle. From bdc881cb55ff3676f642edc5ca4f01370c3c601f Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 17:57:36 -0400 Subject: [PATCH 16/23] adjust CI --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b0edb49e5..a6aa99495 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -199,6 +199,7 @@ jobs: run: yarn workspaces run compile # compile all packages - name: Run tests with coverage on Arbitrum env: + FORK_NETWORK: 'arbitrum' MOCHA_REPORTER: dot MOCHA_RETRY_COUNT: 2 ARBITRUM_NODE_URL: ${{ secrets.ARBITRUM_NODE_URL }} @@ -211,6 +212,7 @@ jobs: path: ./packages/perennial-account/coverage/lcov.info - name: Run tests on Base env: + FORK_NETWORK: 'base' MOCHA_REPORTER: dot MOCHA_RETRY_COUNT: 2 BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} From d6b935e3da655850abfd3502b988ab05b7d42748 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 18:21:27 -0400 Subject: [PATCH 17/23] commented-out new thing --- .github/workflows/CI.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a6aa99495..a7e945ba7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -210,14 +210,14 @@ jobs: with: name: account_integration_test_coverage path: ./packages/perennial-account/coverage/lcov.info - - name: Run tests on Base - env: - FORK_NETWORK: 'base' - MOCHA_REPORTER: dot - MOCHA_RETRY_COUNT: 2 - BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} - run: | - yarn workspace @perennial/account run test:integration + # - name: Run tests on Base + # env: + # FORK_NETWORK: 'base' + # MOCHA_REPORTER: dot + # MOCHA_RETRY_COUNT: 2 + # BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} + # run: | + # yarn workspace @perennial/account run test:integrationBase # [ORACLE] oracle-unit-test: From f8eafdc68154e7b8442bbdf9d842b1de610d3daa Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 18:44:00 -0400 Subject: [PATCH 18/23] uncommented without extra env --- .github/workflows/CI.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a7e945ba7..5fc4047c0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -199,7 +199,6 @@ jobs: run: yarn workspaces run compile # compile all packages - name: Run tests with coverage on Arbitrum env: - FORK_NETWORK: 'arbitrum' MOCHA_REPORTER: dot MOCHA_RETRY_COUNT: 2 ARBITRUM_NODE_URL: ${{ secrets.ARBITRUM_NODE_URL }} @@ -210,14 +209,13 @@ jobs: with: name: account_integration_test_coverage path: ./packages/perennial-account/coverage/lcov.info - # - name: Run tests on Base - # env: - # FORK_NETWORK: 'base' - # MOCHA_REPORTER: dot - # MOCHA_RETRY_COUNT: 2 - # BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} - # run: | - # yarn workspace @perennial/account run test:integrationBase + - name: Run tests on Base + env: + MOCHA_REPORTER: dot + MOCHA_RETRY_COUNT: 2 + BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} + run: | + yarn workspace @perennial/account run test:integrationBase # [ORACLE] oracle-unit-test: From 8e0c7b56c6bcb6ebc297767f615228f6e294f74c Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 19:48:07 -0400 Subject: [PATCH 19/23] enable base tests in ci --- .github/workflows/CI.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5fc4047c0..d828f04f6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -359,7 +359,7 @@ jobs: run: yarn --frozen-lockfile - name: Compile run: yarn workspaces run compile # compile all packages - - name: Run tests + - name: Run tests with coverage on Arbitrum env: MOCHA_REPORTER: dot MOCHA_RETRY_COUNT: 2 @@ -371,6 +371,13 @@ jobs: with: name: order_integration_test_coverage path: ./packages/perennial-order/coverage/lcov.info + - name: Run tests on Base + env: + MOCHA_REPORTER: dot + MOCHA_RETRY_COUNT: 2 + BASE_NODE_URL: ${{ secrets.BASE_NODE_URL }} + run: | + yarn workspace @perennial/order run test:integrationBase # [VAULT] vault-unit-test: From 7bd71de3480751396e17373868343bd973879a3c Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Fri, 18 Oct 2024 21:06:13 -0400 Subject: [PATCH 20/23] reenable other ci jobs --- .github/workflows/CI.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d828f04f6..49778b4f3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -40,7 +40,6 @@ jobs: # [CORE] core-unit-test: - if: false name: '[Core] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -78,7 +77,6 @@ jobs: delete-old-comments: true if: ${{ github.event_name == 'pull_request' && env.PARSER_BROKEN != 'true' }} core-integration-test: - if: false name: '[Core] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -117,7 +115,6 @@ jobs: delete-old-comments: true if: ${{ github.event_name == 'pull_request' && env.PARSER_BROKEN != 'true' }} core-combined-test: - if: false name: '[Core] Combined Tests w/ Coverage' runs-on: ubuntu-latest needs: [core-unit-test, core-integration-test] @@ -219,7 +216,6 @@ jobs: # [ORACLE] oracle-unit-test: - if: false name: '[Oracle] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -249,7 +245,6 @@ jobs: name: oracle_unit_test_coverage path: ./packages/perennial-oracle/coverage/lcov.info oracle-integration-test: - if: false name: '[Oracle] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -280,7 +275,6 @@ jobs: name: oracle_integration_test_coverage path: ./packages/perennial-oracle/coverage/lcov.info oracle-integrationSepolia-test: - if: false name: '[Oracle] Sepolia Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -381,7 +375,6 @@ jobs: # [VAULT] vault-unit-test: - if: false name: '[Vault] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -411,7 +404,6 @@ jobs: name: vault_unit_test_coverage path: ./packages/perennial-vault/coverage/lcov.info vault-integration-test: - if: false name: '[Vault] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -444,7 +436,6 @@ jobs: # [EXTENSIONS] extensions-unit-test: - if: false name: '[Extensions] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -474,7 +465,6 @@ jobs: name: extensions_unit_test_coverage path: ./packages/perennial-extensions/coverage/lcov.info extensions-integration-test: - if: false name: '[Extensions] Integration Tests w/ Coverage' runs-on: ubuntu-latest steps: @@ -507,7 +497,6 @@ jobs: # [VERIFIER] verifier-unit-test: - if: false name: '[Verifier] Unit Tests w/ Coverage' runs-on: ubuntu-latest steps: From 6b3656de96b4688425605d8a2a95e4b99cef6af8 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Mon, 21 Oct 2024 09:28:31 -0400 Subject: [PATCH 21/23] tidy --- .../test/helpers/arbitrumHelpers.ts | 3 ++- .../test/helpers/baseHelpers.ts | 3 ++- .../test/integration/Arbitrum.test.ts | 16 ++++++---------- .../test/integration/Controller.test.ts | 3 +-- .../test/integration/Optimism.test.ts | 17 ++++++----------- 5 files changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/perennial-account/test/helpers/arbitrumHelpers.ts b/packages/perennial-account/test/helpers/arbitrumHelpers.ts index 827bc153f..8a85f06e5 100644 --- a/packages/perennial-account/test/helpers/arbitrumHelpers.ts +++ b/packages/perennial-account/test/helpers/arbitrumHelpers.ts @@ -10,6 +10,7 @@ import { Controller, Controller_Arbitrum, Controller_Arbitrum__factory, + IEmptySetReserve, IEmptySetReserve__factory, IERC20Metadata, IERC20Metadata__factory, @@ -88,7 +89,7 @@ export async function fundWalletUSDC( await usdc.transfer(wallet.address, amount, overrides ?? {}) } -export function getDSUReserve(owner: SignerWithAddress) { +export function getDSUReserve(owner: SignerWithAddress): IEmptySetReserve { return IEmptySetReserve__factory.connect(DSU_RESERVE, owner) } diff --git a/packages/perennial-account/test/helpers/baseHelpers.ts b/packages/perennial-account/test/helpers/baseHelpers.ts index 5f83d2177..bea56008b 100644 --- a/packages/perennial-account/test/helpers/baseHelpers.ts +++ b/packages/perennial-account/test/helpers/baseHelpers.ts @@ -10,6 +10,7 @@ import { Controller, Controller_Optimism, Controller_Optimism__factory, + IEmptySetReserve, IEmptySetReserve__factory, IERC20Metadata, IERC20Metadata__factory, @@ -93,7 +94,7 @@ export async function fundWalletUSDC( await usdc.transfer(wallet.address, amount, overrides ?? {}) } -export function getDSUReserve(owner: SignerWithAddress) { +export function getDSUReserve(owner: SignerWithAddress): IEmptySetReserve { return IEmptySetReserve__factory.connect(DSU_RESERVE, owner) } diff --git a/packages/perennial-account/test/integration/Arbitrum.test.ts b/packages/perennial-account/test/integration/Arbitrum.test.ts index 7f73886e2..f689f5a64 100644 --- a/packages/perennial-account/test/integration/Arbitrum.test.ts +++ b/packages/perennial-account/test/integration/Arbitrum.test.ts @@ -45,22 +45,18 @@ async function deployProtocol( oracleFactory, pythOracleFactory, marketFactory, - ethMarket: undefined, // TODO: style: inlining these was difficult to read; set below - btcMarket: undefined, + ethMarket: createMarketETH + ? await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) + : undefined, + btcMarket: createMarketBTC + ? await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) + : undefined, chainlinkKeptFeed, dsuReserve: getDSUReserve(owner), fundWalletDSU, fundWalletUSDC, } - if (createMarketETH) { - deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) - } - - if (createMarketBTC) { - deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) - } - return deployment } diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index 06fdcf63f..32fbc2aa7 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -79,9 +79,8 @@ export function RunControllerBaseTests( receiver: SignerWithAddress, timestamp = currentTime, price = lastPrice, - keeperOracle = deployment.ethMarket!.keeperOracle, ) { - await advanceToPrice(keeperOracle, receiver, timestamp, price, TX_OVERRIDES) + await advanceToPrice(deployment.ethMarket!.keeperOracle, receiver, timestamp, price, TX_OVERRIDES) await ethMarket.settle(user.address, TX_OVERRIDES) } diff --git a/packages/perennial-account/test/integration/Optimism.test.ts b/packages/perennial-account/test/integration/Optimism.test.ts index e73ec510d..c0d143d88 100644 --- a/packages/perennial-account/test/integration/Optimism.test.ts +++ b/packages/perennial-account/test/integration/Optimism.test.ts @@ -39,22 +39,18 @@ async function deployProtocol( oracleFactory, pythOracleFactory, marketFactory, - ethMarket: undefined, // TODO: style: inlining these was difficult to read; set below - btcMarket: undefined, + ethMarket: createMarketETH + ? await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) + : undefined, + btcMarket: createMarketBTC + ? await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) + : undefined, chainlinkKeptFeed, dsuReserve: getDSUReserve(owner), fundWalletDSU, fundWalletUSDC, } - if (createMarketETH) { - deployment.ethMarket = await setupMarketETH(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) - } - - if (createMarketBTC) { - deployment.btcMarket = await setupMarketBTC(owner, oracleFactory, pythOracleFactory, marketFactory, dsu, overrides) - } - return deployment } @@ -112,7 +108,6 @@ async function mockGasInfo() { } if (process.env.FORK_NETWORK === 'base') { - // TODO: Would it be faster to deploy the protocol once with both markets, and let each test suite take their own snapshots? RunAccountTests(deployProtocol, deployInstance) RunControllerBaseTests(deployProtocol) RunIncentivizedTests('Controller_Optimism', deployProtocol, deployInstance, mockGasInfo) From 637598aab63135f946f1ffeed97eb7a340bc1e4c Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Mon, 21 Oct 2024 10:19:42 -0400 Subject: [PATCH 22/23] moar linter fixes --- .../contracts/Controller.sol | 6 ++--- .../test/integration/Controller.test.ts | 27 ++++++++++++++----- .../Controller_Incentivized.test.ts | 21 ++++++++++++--- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/perennial-account/contracts/Controller.sol b/packages/perennial-account/contracts/Controller.sol index 994750882..546d3a031 100644 --- a/packages/perennial-account/contracts/Controller.sol +++ b/packages/perennial-account/contracts/Controller.sol @@ -25,10 +25,10 @@ import { Withdrawal, WithdrawalLib } from "./types/Withdrawal.sol"; /// without keeper compensation. No message relaying facilities are provided. contract Controller is Factory, IController { // used for deterministic address creation through create2 - bytes32 constant SALT = keccak256("Perennial V2 Collateral Accounts"); + bytes32 public constant SALT = keccak256("Perennial V2 Collateral Accounts"); - uint256 constant MAX_GROUPS_PER_OWNER = 8; - uint256 constant MAX_MARKETS_PER_GROUP = 4; + uint256 public constant MAX_GROUPS_PER_OWNER = 8; + uint256 public constant MAX_MARKETS_PER_GROUP = 4; /// @dev USDC stablecoin address Token6 public immutable USDC; // solhint-disable-line var-name-mixedcase diff --git a/packages/perennial-account/test/integration/Controller.test.ts b/packages/perennial-account/test/integration/Controller.test.ts index 32fbc2aa7..ae8e8b96c 100644 --- a/packages/perennial-account/test/integration/Controller.test.ts +++ b/packages/perennial-account/test/integration/Controller.test.ts @@ -18,7 +18,7 @@ import { } from '../../types/generated' import { IMarket, IMarketFactory } from '@perennial/core/types/generated' import { signDeployAccount, signMarketTransfer, signRebalanceConfigChange, signWithdrawal } from '../helpers/erc712' -import { advanceToPrice, deployController, DeploymentVars } from '../helpers/setupHelpers' +import { advanceToPrice, deployController, DeploymentVars, MarketWithOracle } from '../helpers/setupHelpers' const { ethers } = HRE @@ -41,6 +41,7 @@ export function RunControllerBaseTests( let verifier: IAccountVerifier let marketFactory: IMarketFactory let ethMarket: IMarket + let ethMarketDeployment: MarketWithOracle let accountA: Account let owner: SignerWithAddress let userA: SignerWithAddress @@ -80,7 +81,7 @@ export function RunControllerBaseTests( timestamp = currentTime, price = lastPrice, ) { - await advanceToPrice(deployment.ethMarket!.keeperOracle, receiver, timestamp, price, TX_OVERRIDES) + await advanceToPrice(ethMarketDeployment.keeperOracle, receiver, timestamp, price, TX_OVERRIDES) await ethMarket.settle(user.address, TX_OVERRIDES) } @@ -181,7 +182,12 @@ export function RunControllerBaseTests( marketFactory = deployment.marketFactory dsu = deployment.dsu usdc = deployment.usdc - ethMarket = deployment.ethMarket!.market + if (deployment.ethMarket) { + ethMarketDeployment = deployment.ethMarket + ethMarket = ethMarketDeployment.market + } else { + throw new Error('ETH market not created') + } // deploy controller controller = await deployController( @@ -196,13 +202,13 @@ export function RunControllerBaseTests( // set initial price await advanceToPrice( - deployment.ethMarket!.keeperOracle, + ethMarketDeployment.keeperOracle, receiver, currentTime, parse6decimal('3116.734999'), TX_OVERRIDES, ) - lastPrice = (await deployment.ethMarket!.oracle.status())[0].price + lastPrice = (await ethMarketDeployment.oracle.status())[0].price // create a collateral account for userA with 15k collateral in it await fundWallet(userA) @@ -227,11 +233,18 @@ export function RunControllerBaseTests( describe('#rebalance', () => { let btcMarket: IMarket + let btcMarketDeployment: MarketWithOracle beforeEach(async () => { // create another market, including requisite oracles, and set initial price - btcMarket = deployment.btcMarket!.market - const btcKeeperOracle = deployment.btcMarket!.keeperOracle + if (deployment.btcMarket) { + btcMarketDeployment = deployment.btcMarket + btcMarket = btcMarketDeployment.market + } else { + throw new Error('BTC market not created') + } + + const btcKeeperOracle = btcMarketDeployment.keeperOracle await advanceToPrice(btcKeeperOracle, receiver, currentTime, parse6decimal('60606.369'), TX_OVERRIDES) // configure a group with both markets diff --git a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts index a5e334636..4343e36a2 100644 --- a/packages/perennial-account/test/integration/Controller_Incentivized.test.ts +++ b/packages/perennial-account/test/integration/Controller_Incentivized.test.ts @@ -187,8 +187,21 @@ export function RunIncentivizedTests( dsu = deployment.dsu usdc = deployment.usdc marketFactory = deployment.marketFactory - ethMarket = deployment.ethMarket!.market - btcMarket = deployment.btcMarket!.market + let ethMarketDeployment + if (deployment.ethMarket) { + ethMarketDeployment = deployment.ethMarket + ethMarket = deployment.ethMarket.market + } else { + throw new Error('BTC market not created') + } + let btcMarketDeployment + if (deployment.btcMarket) { + btcMarketDeployment = deployment.btcMarket + btcMarket = btcMarketDeployment.market + } else { + throw new Error('BTC market not created') + } + ;[controller, accountVerifier] = await deployInstance( owner, deployment.marketFactory, @@ -197,14 +210,14 @@ export function RunIncentivizedTests( ) await advanceToPrice( - deployment.ethMarket!.keeperOracle, + ethMarketDeployment.keeperOracle, receiver, currentTime, parse6decimal('3113.7128'), TX_OVERRIDES, ) await advanceToPrice( - deployment.btcMarket!.keeperOracle, + btcMarketDeployment.keeperOracle, receiver, currentTime, parse6decimal('57575.464'), From c26c8395e78834df9f3b9a2dce141a27837417f5 Mon Sep 17 00:00:00 2001 From: Ed Noepel Date: Tue, 22 Oct 2024 09:32:50 -0400 Subject: [PATCH 23/23] adjust whitespace --- packages/perennial-order/contracts/Manager_Arbitrum.sol | 3 +-- packages/perennial-order/contracts/Manager_Optimism.sol | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/perennial-order/contracts/Manager_Arbitrum.sol b/packages/perennial-order/contracts/Manager_Arbitrum.sol index a60785bb5..c8c6744da 100644 --- a/packages/perennial-order/contracts/Manager_Arbitrum.sol +++ b/packages/perennial-order/contracts/Manager_Arbitrum.sol @@ -16,8 +16,7 @@ contract Manager_Arbitrum is Manager, Kept_Arbitrum { IEmptySetReserve reserve, IMarketFactory marketFactory, IOrderVerifier verifier - ) - Manager(usdc, dsu, reserve, marketFactory, verifier) {} + ) Manager(usdc, dsu, reserve, marketFactory, verifier) {} /// @dev Use the Kept_Arbitrum implementation for calculating the dynamic fee function _calldataFee( diff --git a/packages/perennial-order/contracts/Manager_Optimism.sol b/packages/perennial-order/contracts/Manager_Optimism.sol index 8c26d9d0a..1e948e1fc 100644 --- a/packages/perennial-order/contracts/Manager_Optimism.sol +++ b/packages/perennial-order/contracts/Manager_Optimism.sol @@ -16,8 +16,7 @@ contract Manager_Optimism is Manager, Kept_Optimism { IEmptySetReserve reserve, IMarketFactory marketFactory, IOrderVerifier verifier - ) - Manager(usdc, dsu, reserve, marketFactory, verifier) {} + ) Manager(usdc, dsu, reserve, marketFactory, verifier) {} /// @dev Use the Kept_Optimism implementation for calculating the dynamic fee function _calldataFee(