From 83f31afd903ad0abc144829a27ad80f8129d4ea6 Mon Sep 17 00:00:00 2001 From: jahabeebs <47253537+jahabeebs@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:21:23 -0500 Subject: [PATCH] fix: separate service provider address logic from other accounts --- apps/agent/config.example.yml | 1 - apps/agent/src/index.ts | 4 +- .../e2e/scenarios/01_happy_path/index.spec.ts | 22 +- .../e2e/utils/prophet-e2e-scaffold/eboCore.ts | 232 +++++++++++++----- 4 files changed, 184 insertions(+), 75 deletions(-) diff --git a/apps/agent/config.example.yml b/apps/agent/config.example.yml index a52c357..31327e7 100644 --- a/apps/agent/config.example.yml +++ b/apps/agent/config.example.yml @@ -31,7 +31,6 @@ blockNumberService: processor: msBetweenChecks: 1000 # Interval between periodic checks (ms) accountingModules: - requestModule: "0x1234567890123456789012345678901234567890" # Address of the Request module responseModule: "0x1234567890123456789012345678901234567890" # Address of the Response module escalationModule: "0x1234567890123456789012345678901234567890" # Address of the Escalation module diff --git a/apps/agent/src/index.ts b/apps/agent/src/index.ts index 1a63c24..900d277 100644 --- a/apps/agent/src/index.ts +++ b/apps/agent/src/index.ts @@ -7,11 +7,11 @@ import { ProtocolProvider, } from "@ebo-agent/automated-dispute"; import { BlockNumberService } from "@ebo-agent/blocknumber"; -import { Logger, NotificationService, stringify } from "@ebo-agent/shared"; +import { NotificationService, stringify } from "@ebo-agent/shared"; import { config } from "./config/index.js"; -const logger = Logger.getInstance(); +const logger = console; const main = async (): Promise => { logger.debug("Initializing agent..."); diff --git a/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts b/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts index dde2473..b041516 100644 --- a/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts +++ b/apps/agent/test/e2e/scenarios/01_happy_path/index.spec.ts @@ -145,8 +145,12 @@ describe.sequential("single agent", () => { }), ]; + const serviceProvider = accounts[0].account; + const otherAccounts = accounts.slice(1).map((acc) => acc.account); + await setUpProphet({ - accounts: accounts.map((account) => account.account), + serviceProvider: serviceProvider, + otherAccounts: otherAccounts, arbitratorAddress: ARBITRATOR_ADDRESS, grtAddress: GRT_CONTRACT_ADDRESS, horizonStakingAddress: HORIZON_STAKING_ADDRESS, @@ -166,10 +170,14 @@ describe.sequential("single agent", () => { afterEach(async () => { await l2ProtocolAnvil.stop(); - await killAgent({ - process: agent, - configPath: tmpConfigFile, - }); + if (fs.existsSync(tmpConfigFile)) { + await killAgent({ + process: agent, + configPath: tmpConfigFile, + }); + } else { + console.warn(`Warning: ${tmpConfigFile} does not exist. Skipping agent kill.`); + } }); test("basic flow", { timeout: E2E_TEST_TIMEOUT }, async () => { @@ -227,6 +235,9 @@ describe.sequential("single agent", () => { retryInterval: 500, }, }, + accessControl: { + serviceProviderAddress: undefined, + }, }, blockNumberService: { blockmetaConfig: { @@ -941,6 +952,7 @@ describe.sequential("single agent", () => { retryInterval: 500, }, }, + privateKey: accounts[0].privateKey, }, blockNumberService: { blockmetaConfig: { diff --git a/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts b/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts index c90cf54..9db2363 100644 --- a/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts +++ b/apps/agent/test/e2e/utils/prophet-e2e-scaffold/eboCore.ts @@ -9,7 +9,6 @@ import { createTestClient, createWalletClient, encodeFunctionData, - formatEther, Hex, http, HttpTransport, @@ -22,25 +21,12 @@ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { AnvilClient } from "./anvil.js"; -/** - * Encode the function to transfer ERC20 GRT token to a specified `address` - * - * @param address token receiver's address - * @returns the abi encoded transfer function - */ -function transferGrtToAccount(address: Address, amount: bigint) { - return encodeFunctionData({ - abi: parseAbi(["function transfer(address, uint256)"]), - args: [address, amount], - }); -} - /** * Fund `account` with `amount` GRT tokens by transferring from a known holder. * * @param account account to fund - * @param amount amount of GRT to fund `account` with * @param anvilClient wallet client for anvil to use to impersonate the GRT holder + * @param grt GRT token details * @param grt.holderAddress address of the GRT tokens holder * @param grt.contractAddress address of the GRT contract address */ @@ -64,27 +50,30 @@ async function fundAccount( value: parseEther("100"), }); - console.log(`Added 1 ETH to ${grt.holderAddress}.`); + console.log(`Added 100 ETH to ${grt.holderAddress}.`); await anvilClient.setBalance({ address: account.address, value: parseEther("100"), }); - console.log(`Added 1 ETH to ${account.address}.`); + console.log(`Added 100 ETH to ${account.address}.`); console.log(`Sending GRT tokens from ${grt.holderAddress} to ${account.address}...`); - const hash = await anvilClient.sendTransaction({ + const transferHash = await anvilClient.sendTransaction({ account: grt.holderAddress, to: grt.contractAddress, - data: transferGrtToAccount(account.address, grt.fundAmount), + data: encodeFunctionData({ + abi: parseAbi(["function transfer(address, uint256)"]), + args: [account.address, grt.fundAmount], + }), }); console.log("Waiting for transaction receipt..."); await anvilClient.waitForTransactionReceipt({ - hash: hash, + hash: transferHash, }); console.log(`GRT tokens sent.`); @@ -224,8 +213,10 @@ export async function deployContracts( interface SetUpProphetInput { /** Chains to add to EBORequestCreator contract */ chainsToAdd: Caip2ChainId[]; - /** Accounts to approve modules for */ - accounts: Account[]; + /** Accounts to stake GRT for */ + otherAccounts: Account[]; + /** Service provider account */ + serviceProvider: Account; /** Map of deployed contracts */ deployedContracts: DeployContractsOutput; /** GRT amount to provision account with to be able to bond tokens throughout its operation */ @@ -240,6 +231,20 @@ interface SetUpProphetInput { anvilClient: AnvilClient; } +/** + * Encode the function to approve ERC20 GRT token spending by a specified `spender`. + * + * @param spender - The address authorized to spend the tokens. + * @param amount - The amount of tokens to approve. + * @returns The ABI-encoded data for the `approve` function. + */ +function approveSpender(spender: Address, amount: bigint) { + return encodeFunctionData({ + abi: parseAbi(["function approve(address spender, uint256 amount)"]), + args: [spender, amount], + }); +} + /** * Set up Prophet and EBO contracts with basic data to start operating with them * @@ -247,17 +252,29 @@ interface SetUpProphetInput { */ export async function setUpProphet(input: SetUpProphetInput) { const { + serviceProvider, + otherAccounts, chainsToAdd, - accounts, deployedContracts, anvilClient, grtProvisionAmount: bondAmount, } = input; const { arbitratorAddress, grtAddress, horizonStakingAddress } = input; - await approveEboProphetModules(accounts, deployedContracts, anvilClient); + await approveEboProphetModules( + [serviceProvider, ...otherAccounts], + deployedContracts, + anvilClient, + ); + + if (!deployedContracts["HorizonAccountingExtension"]) { + throw new Error("HorizonAccountingExtension contract not found in deployed contracts"); + } + + // Stake GRT and set operator authorization only for the service provider await stakeGrtWithProvision( - accounts, + serviceProvider, + otherAccounts, { grt: grtAddress, horizonStaking: horizonStakingAddress, @@ -266,6 +283,7 @@ export async function setUpProphet(input: SetUpProphetInput) { bondAmount, anvilClient, ); + await addEboRequestCreatorChains( chainsToAdd, deployedContracts, @@ -275,12 +293,11 @@ export async function setUpProphet(input: SetUpProphetInput) { } /** - * Approve EBO core accounting modules usage. + * Approve EBO core accounting modules usage for all accounts. * - * @param accounts accounts to approve modules for - * @param deployedContracts addresses of deployed contracts - * @param clients.public a public viem client - * @param clients.wallet a wallet viem client + * @param accounts - All accounts to approve modules for. + * @param deployedContracts - Addresses of deployed contracts. + * @param anvilClient - Anvil client for transactions. */ async function approveEboProphetModules( accounts: Account[], @@ -293,6 +310,7 @@ async function approveEboProphetModules( deployedContracts["BondedResponseModule"], deployedContracts["BondEscalationModule"], ]; + for (const account of accounts) { for (const module of modulesToBeApproved) { const hash = await anvilClient.sendTransaction({ @@ -308,6 +326,7 @@ async function approveEboProphetModules( hash: hash, }); } + const approvedModules = await anvilClient.readContract({ address: deployedContracts["HorizonAccountingExtension"] as Address, abi: parseAbi(["function approvedModules(address) external view returns (address[])"]), @@ -321,7 +340,8 @@ async function approveEboProphetModules( } async function stakeGrtWithProvision( - accounts: Account[], + serviceProvider: Account, + otherAccounts: Account[], addresses: { grt: Address; horizonStaking: Address; @@ -334,24 +354,20 @@ async function stakeGrtWithProvision( const { grt, horizonStaking, horizonAccountingExtension } = addresses; - for (const account of accounts) { - console.log(`Approving GRT txs on ${account.address} to ${horizonStaking}`); + // Stake GRT for other accounts + for (const account of otherAccounts) { + console.log(`Setting up account ${account.address}...`); + // Approve GRT spending by horizonStaking const approveHash = await anvilClient.sendTransaction({ account: account, to: grt, - data: encodeFunctionData({ - abi: parseAbi(["function approve(address, uint256)"]), - args: [horizonStaking, grtProvisionAmount], - }), - }); - - await anvilClient.waitForTransactionReceipt({ - hash: approveHash, + data: approveSpender(horizonStaking, grtProvisionAmount), }); + await anvilClient.waitForTransactionReceipt({ hash: approveHash }); + console.log(`GRT spending approved for ${account.address}`); - console.log(`Staking for ${account.address} ${formatEther(grtProvisionAmount)} GRT...`); - + // Stake GRT const stakeHash = await anvilClient.sendTransaction({ account: account, to: horizonStaking, @@ -360,35 +376,117 @@ async function stakeGrtWithProvision( args: [grtProvisionAmount], }), }); + await anvilClient.waitForTransactionReceipt({ hash: stakeHash }); + console.log(`GRT staked for ${account.address}`); + } - await anvilClient.waitForTransactionReceipt({ - hash: stakeHash, - }); + // **Stake GRT for the Service Provider** + console.log(`Setting up service provider account ${serviceProvider.address}...`); - const provisionHash = await anvilClient.sendTransaction({ - account: account, - to: horizonStaking, - data: encodeFunctionData({ - abi: parseAbi(["function provision(address, address, uint256, uint32, uint64)"]), - args: [ - account.address, - horizonAccountingExtension, - grtProvisionAmount, - // TODO: use contract call to get this value - // https://github.com/defi-wonderland/EBO-core/blob/175bcd57c3254a90dd6fcbf53b3db3359085551f/src/contracts/HorizonAccountingExtension.sol#L38C26-L38C42 - 1_000_000, - // https://github.com/defi-wonderland/EBO-core/blob/175bcd57c3254a90dd6fcbf53b3db3359085551f/script/Constants.sol#L21 - BigInt(60 * 60 * 24 * 3), // 3 days - ], - }), - }); + // Approve GRT spending by horizonStaking + const approveHashSP = await anvilClient.sendTransaction({ + account: serviceProvider, + to: grt, + data: approveSpender(horizonStaking, grtProvisionAmount), + }); + await anvilClient.waitForTransactionReceipt({ hash: approveHashSP }); + console.log(`GRT spending approved for ${serviceProvider.address}`); + + // Stake GRT + const stakeHashSP = await anvilClient.sendTransaction({ + account: serviceProvider, + to: horizonStaking, + data: encodeFunctionData({ + abi: parseAbi(["function stake(uint256)"]), + args: [grtProvisionAmount], + }), + }); + await anvilClient.waitForTransactionReceipt({ hash: stakeHashSP }); + console.log(`GRT staked for ${serviceProvider.address}`); - await anvilClient.waitForTransactionReceipt({ - hash: provisionHash, - }); + // Provision GRT + console.log( + `Provisioning GRT from ${serviceProvider.address} to ${horizonAccountingExtension}...`, + ); + const provisionHash = await anvilClient.sendTransaction({ + account: serviceProvider, + to: horizonStaking, + data: encodeFunctionData({ + abi: parseAbi(["function provision(address, address, uint256, uint32, uint64)"]), + args: [ + serviceProvider.address, // service provider + horizonAccountingExtension, // verifier + grtProvisionAmount, + // TODO: use contract call to get this value + // https://github.com/defi-wonderland/EBO-core/blob/175bcd57c3254a90dd6fcbf53b3db3359085551f/src/contracts/HorizonAccountingExtension.sol#L38C26-L38C42 + 1_000_000, // maxVerifierCut (PPM) + // https://github.com/defi-wonderland/EBO-core/blob/175bcd57c3254a90dd6fcbf53b3db3359085551f/script/Constants.sol#L21 + BigInt(60 * 60 * 24 * 3), // thawingPeriod (3 days) + ], + }), + }); + const provisionReceipt = await anvilClient.waitForTransactionReceipt({ hash: provisionHash }); + console.log(`Provision transaction status: ${provisionReceipt.status}`); + + // Verify the provision exists + const provisionAbi = parseAbi([ + "struct Provision { uint256 tokens; uint256 tokensThawing; uint256 sharesThawing; uint32 maxVerifierCut; uint64 thawingPeriod; uint64 createdAt; uint32 maxVerifierCutPending; uint64 thawingPeriodPending; }", + "function getProvision(address serviceProvider, address verifier) view returns (Provision)", + ]); + + const provision = await anvilClient.readContract({ + address: horizonStaking, + abi: provisionAbi, + functionName: "getProvision", + args: [serviceProvider.address, horizonAccountingExtension], + }); + console.log("Provision details:", provision); + + // Set operator authorization only for the service provider + console.log( + `Setting operator authorization for service provider ${serviceProvider.address}...`, + ); + const setOperatorHash = await anvilClient.sendTransaction({ + account: serviceProvider, + to: horizonStaking, + data: encodeFunctionData({ + abi: parseAbi([ + "function setOperator(address verifier, address operator, bool allowed) external", + ]), + args: [ + horizonAccountingExtension, // verifier + serviceProvider.address, // operator + true, // allowed + ], + }), + }); + const receiptSetOp = await anvilClient.waitForTransactionReceipt({ hash: setOperatorHash }); + + if (receiptSetOp.status !== "success") { + throw new Error(`Transaction failed: setOperator for ${serviceProvider.address}`); + } + console.log(`Operator authorization set for ${serviceProvider.address}`); + + // Verify authorization + const preProvisionAuth = await anvilClient.readContract({ + address: horizonStaking, + abi: parseAbi([ + "function isAuthorized(address serviceProvider, address verifier, address operator) view returns (bool)", + ]), + functionName: "isAuthorized", + args: [ + serviceProvider.address, // service provider + horizonAccountingExtension, // verifier + serviceProvider.address, // operator + ], + }); + console.log(`Pre-provision authorization status: ${preProvisionAuth}`); - console.log(`Stake and provision done for ${account.address}`); + if (!preProvisionAuth) { + throw new Error(`Failed to set operator authorization for ${serviceProvider.address}`); } + + console.log(`Setup completed for ${serviceProvider.address}`); } /**