From 15d272f3cd8553fd32d5aa3361f4c142ca1093c7 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 7 Feb 2024 12:11:16 +0800 Subject: [PATCH 01/10] precom changes --- auction-server/src/api/marketplace.rs | 55 +++++++++++++++++++----- auction-server/src/state.rs | 2 +- per_sdk/protocols/token_vault_monitor.py | 2 +- per_sdk/searcher/simple_searcher.py | 4 ++ 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/auction-server/src/api/marketplace.rs b/auction-server/src/api/marketplace.rs index 4485fe01..8ca4b727 100644 --- a/auction-server/src/api/marketplace.rs +++ b/auction-server/src/api/marketplace.rs @@ -147,7 +147,7 @@ pub async fn submit_opportunity( receipt_tokens, bidders: Default::default(), }; - + verify_opportunity( verified_opportunity.clone(), chain_store, @@ -156,12 +156,40 @@ pub async fn submit_opportunity( .await .map_err(|e| RestError::InvalidOpportunity(e.to_string()))?; + let mut opportunities_existing = Vec::new(); + + if store + .liquidation_store + .opportunities + .read() + .await + .contains_key(&opportunity.permission_key) + { + opportunities_existing = + store.liquidation_store.opportunities.read().await[&opportunity.permission_key].clone(); + let opportunity_top = opportunities_existing[0].clone(); + // check if exact same opportunity exists already + if opportunity_top.chain_id == opportunity.chain_id + && opportunity_top.contract == opportunity.contract + && opportunity_top.calldata == opportunity.calldata + && opportunity_top.value == opportunity.value + && opportunity_top.repay_tokens == repay_tokens + && opportunity_top.receipt_tokens == receipt_tokens + { + return Err(RestError::BadParameters( + "Duplicate opportunity submission".to_string(), + )); + } + } + + opportunities_existing.push(verified_opportunity.clone()); + store .liquidation_store .opportunities .write() .await - .insert(opportunity.permission_key.clone(), verified_opportunity); + .insert(opportunity.permission_key.clone(), opportunities_existing); Ok(id.to_string()) } @@ -181,20 +209,20 @@ pub async fn fetch_opportunities( .await .values() .cloned() - .map(|opportunity| LiquidationOpportunityWithId { - opportunity_id: opportunity.id, + .map(|opportunities| LiquidationOpportunityWithId { + opportunity_id: opportunities[0].id, opportunity: LiquidationOpportunity { - permission_key: opportunity.permission_key, - chain_id: opportunity.chain_id, - contract: opportunity.contract, - calldata: opportunity.calldata, - value: opportunity.value, - repay_tokens: opportunity + permission_key: opportunities[0].permission_key, + chain_id: opportunities[0].chain_id, + contract: opportunities[0].contract, + calldata: opportunities[0].calldata, + value: opportunities[0].value, + repay_tokens: opportunities[0] .repay_tokens .into_iter() .map(TokenQty::from) .collect(), - receipt_tokens: opportunity + receipt_tokens: opportunities[0] .receipt_tokens .into_iter() .map(TokenQty::from) @@ -252,6 +280,11 @@ pub async fn bid_opportunity( .clone(); + // TODO: delete these prints + tracing::info!("Opportunity: {:?}", liquidation.id); + tracing::info!("Opp bid: {:?}", opportunity_bid.opportunity_id); + + // this check fails whenever opportunity ID is updated by the protocol monitor, which it can be often if liquidation.id != opportunity_bid.opportunity_id { return Err(RestError::BadParameters( "Invalid opportunity_id".to_string(), diff --git a/auction-server/src/state.rs b/auction-server/src/state.rs index c86f1236..3eb4b637 100644 --- a/auction-server/src/state.rs +++ b/auction-server/src/state.rs @@ -67,7 +67,7 @@ pub struct ChainStore { #[derive(Default)] pub struct LiquidationStore { - pub opportunities: RwLock>, + pub opportunities: RwLock>>, } pub struct Store { diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index 813ca7cf..6e9e866e 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -206,7 +206,7 @@ async def get_liquidation_opportunities(self) -> list[LiquidationOpportunity]: logger.debug(f"Account {account['account_number']} health: {health}") if ( value_debt * int(account["min_health_ratio"]) - > value_collateral * 10**18 + > value_collateral * 10 ** 18 ): price_updates = [price_collateral, price_debt] liquidatable.append(self.create_liquidation_opp(account, price_updates)) diff --git a/per_sdk/searcher/simple_searcher.py b/per_sdk/searcher/simple_searcher.py index 0168b94d..3ede95a2 100644 --- a/per_sdk/searcher/simple_searcher.py +++ b/per_sdk/searcher/simple_searcher.py @@ -145,6 +145,7 @@ async def main(): continue logger.debug("Found %d liquidation opportunities", len(accounts_liquidatable)) + for liquidation_opp in accounts_liquidatable: bid_info = assess_liquidation_opportunity(args.bid, liquidation_opp) @@ -153,6 +154,9 @@ async def main(): tx = create_liquidation_transaction( liquidation_opp, sk_liquidator, bid_info ) + import pdb + + pdb.set_trace() resp = await client.post(LIQUIDATION_SERVER_ENDPOINT_BID, json=tx) logger.info( From 8ccd3ce01a764170f8f3dd07499a470d46902c8f Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 7 Feb 2024 12:14:27 +0800 Subject: [PATCH 02/10] testing changes to accomodate updates --- per_multicall/README.md | 12 +++++----- per_multicall/remappings.txt | 1 + per_multicall/script/Vault.s.sol | 38 +++++++++++++++++++++++--------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/per_multicall/README.md b/per_multicall/README.md index 03825d1d..2e9eda9e 100644 --- a/per_multicall/README.md +++ b/per_multicall/README.md @@ -26,34 +26,36 @@ $ forge test -vvv --via-ir You can also run a local validator via `anvil --gas-limit 500000000000000000 --block-time 2`, changing the values for the gas limit and block time as desired. Note that if you omit the `--block-time` flag, the local network will create a new block for each transaction (similar to how Optimism created L2 blocks pre-Bedrock). Running `auction_offchain.py` will spit out the final call to `forge script` you should run to send the transaction to the localnet. -To run the script runs in `Vault.s.sol`, you should startup the local validator and create a `.env` file with the `PRIVATE_KEY` env variable which is used for submitting the transactions. Then, run the necessary setup commands: +To run the script runs in `Vault.s.sol`, you should startup the local validator and create a `.env` file with the `PRIVATE_KEY` env variable which is used for submitting the transactions. For localnet, the private key saved should correspond to an address that has a bunch of ETH seeded by Forge, essentially one of the mnemonic wallets when you start up anvil. Then, run the necessary setup commands: 1. Set up contracts and save to an environment JSON. ```shell -$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --sender 0xd6e417287b875a3932c1ff5dcb26d4d2c8b90b40 -vvv --sig 'setUpContracts()' --broadcast +$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --private-key 0xf46ea803192f16ef1c4f1d5fb0d6060535dbd571ea1afc7db6816f28961ba78a -vvv --sig 'setUpContracts()' --broadcast ``` 2. Set oracle prices to allow for vault creation. ```shell -$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --sender 0xd6e417287b875a3932c1ff5dcb26d4d2c8b90b40 -vvv --sig 'setOraclePrice(int64,int64,uint64)' 110 110 190 --broadcast +$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --private-key 0xf46ea803192f16ef1c4f1d5fb0d6060535dbd571ea1afc7db6816f28961ba78a -vvv --sig 'setOraclePrice(int64,int64,uint64)' 110 110 190 --broadcast ``` 3. Vault creation. ```shell -$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --sender 0xd6e417287b875a3932c1ff5dcb26d4d2c8b90b40 -vvv --sig 'setUpVault(uint256,uint256,bool)' 100 80 true --broadcast +$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --private-key 0xf46ea803192f16ef1c4f1d5fb0d6060535dbd571ea1afc7db6816f28961ba78a -vvv --sig 'setUpVault(uint256,uint256,bool)' 100 80 true --broadcast ``` 4. Undercollateralize the vault by moving prices. ```shell -$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --sender 0xd6e417287b875a3932c1ff5dcb26d4d2c8b90b40 --private-key 0xf46ea803192f16ef1c4f1d5fb0d6060535dbd571ea1afc7db6816f28961ba78a -vvv --sig 'setOraclePrice(int64,int64,uint64)' 110 200 200 --broadcast +$ forge script script/Vault.s.sol --via-ir --fork-url http://localhost:8545 --private-key 0xf46ea803192f16ef1c4f1d5fb0d6060535dbd571ea1afc7db6816f28961ba78a -vvv --sig 'setOraclePrice(int64,int64,uint64)' 110 200 200 --broadcast ``` 5. Submit the PER bundle. Run the command spit out by the auction script. Because of the call to `vm.roll`, this essentially does a simulate and can be run repeatedly from this state. +Note that the `--private-key` flag is necessary in order to run some of the commands above; this is because Forge requires specification of a default sender wallet from which the transactions are sent. + In order to enable forge to write to the filesystem (which is needed in order to save some of the variables in the steps above), please navigate to `foundry.toml` and add the following line if it does not already exist: ``` diff --git a/per_multicall/remappings.txt b/per_multicall/remappings.txt index 821595eb..c877de03 100644 --- a/per_multicall/remappings.txt +++ b/per_multicall/remappings.txt @@ -4,3 +4,4 @@ erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ @pythnetwork/pyth-sdk-solidity=node_modules/@pythnetwork/pyth-sdk-solidity/ +solidity-bytes-utils=node_modules/solidity-bytes-utils diff --git a/per_multicall/script/Vault.s.sol b/per_multicall/script/Vault.s.sol index 74e541e5..ee92c294 100644 --- a/per_multicall/script/Vault.s.sol +++ b/per_multicall/script/Vault.s.sol @@ -68,7 +68,7 @@ contract VaultScript is Script { } function deployMockPyth() public returns (address) { - MockPyth mockPyth = new MockPyth(1_000_000, 0); + MockPyth mockPyth = new MockPyth(1_000_000_000_000, 0); console.log("deployed mock pyth contract at", address(mockPyth)); return address(mockPyth); } @@ -235,8 +235,21 @@ contract VaultScript is Script { // instantiate ERC-20 tokens vm.startBroadcast(sksScript[3]); console.log("balance of pk perOperator", addressesScript[3].balance); - token1 = new MyToken("token1", "T1"); - token2 = new MyToken("token2", "T2"); + + // create token price feed IDs--see https://pyth.network/developers/price-feed-ids + // TODO: automate converting bytes32 to string + idToken1 = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace; // ETH/USD + idToken2 = 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43; // BTC/USD + string + memory idToken1Str = "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"; + string + memory idToken2Str = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + console.log("ids of tokens"); + console.logBytes32(idToken1); + console.logBytes32(idToken2); + + token1 = new MyToken(idToken1Str, "T1"); + token2 = new MyToken(idToken2Str, "T2"); console.log("token 1 address", address(token1)); console.log("token 2 address", address(token2)); @@ -266,13 +279,6 @@ contract VaultScript is Script { // mint token 2 to the vault contract (to allow creation of initial vault with outstanding debt position) token2.mint(tokenVaultAddress, qtys[7]); - // create token price feed IDs--see https://pyth.network/developers/price-feed-ids - idToken1 = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace; // ETH/USD - idToken2 = 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43; // BTC/USD - console.log("ids of tokens"); - console.logBytes32(idToken1); - console.logBytes32(idToken2); - // mint token to searchers A and B EOAs token1.mint(address(addressesScript[0]), 20_000_000); token2.mint(address(addressesScript[0]), 20_000_000); @@ -346,6 +352,13 @@ contract VaultScript is Script { bytes32 idToken1Latest = vm.parseJsonBytes32(json, ".idToken1"); bytes32 idToken2Latest = vm.parseJsonBytes32(json, ".idToken2"); + console.log("oracle address:"); + console.log(oracleLatest); + console.log("token 1 id:"); + console.logBytes32(idToken1Latest); + console.log("token 2 id:"); + console.logBytes32(idToken2Latest); + MockPyth oracle = MockPyth(payable(oracleLatest)); // set initial oracle prices @@ -400,6 +413,11 @@ contract VaultScript is Script { IERC20(token2Latest).balanceOf(depositorLatest) ); + address oracleLatest = vm.parseJsonAddress(json, ".oracle"); + MockPyth oracle = MockPyth(payable(oracleLatest)); + console.log(oracleLatest); + console.logInt(oracle.queryPriceFeed(idToken1Latest).price.price); + if (collatT1) { vm.startBroadcast(depositorSkLatest); IERC20(token1Latest).approve(address(tokenVaultLatest), qT1); From 8723a1cebf0368fb76767141f001f9f5633e9fac Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 7 Feb 2024 12:15:05 +0800 Subject: [PATCH 03/10] package changes --- package-lock.json | 583 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 + 2 files changed, 586 insertions(+) diff --git a/package-lock.json b/package-lock.json index ff95bfbb..4df6eae2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,9 @@ "requires": true, "packages": { "": { + "dependencies": { + "ethereum-private-key-to-public-key": "^0.0.5" + }, "devDependencies": { "prettier": "^3.1.1", "prettier-plugin-solidity": "^1.3.1" @@ -15,6 +18,269 @@ "integrity": "sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==", "dev": true }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==", + "dependencies": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/ethereum-private-key-to-public-key": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/ethereum-private-key-to-public-key/-/ethereum-private-key-to-public-key-0.0.5.tgz", + "integrity": "sha512-oSdjEi3BzhG7M5D6ZKZB5zWbINRbJ5lsaROJz+RePRAn8aylm6j93uSqNvzge+kIsslbuu2tXeXgHjEZ/tweRg==", + "dependencies": { + "meow": "^5.0.0", + "secp256k1": "^4.0.2" + }, + "bin": { + "ethereum_private_key_to_public_key": "bin/ethereum_private_key_to_public_key", + "ethereum-private-key-to-public-key": "bin/ethereum_private_key_to_public_key" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", + "dependencies": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -27,6 +293,163 @@ "node": ">=10" } }, + "node_modules/map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "dependencies": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "engines": { + "node": ">=4" + } + }, "node_modules/prettier": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", @@ -59,6 +482,81 @@ "prettier": ">=2.3.0" } }, + "node_modules/quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==", + "dependencies": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "hasInstallScript": true, + "dependencies": { + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -74,17 +572,102 @@ "node": ">=10" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/solidity-comments-extractor": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz", "integrity": "sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==", "dev": true }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", + "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==" + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true + }, + "node_modules/yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dependencies": { + "camelcase": "^4.1.0" + } } } } diff --git a/package.json b/package.json index 19654a1f..145b1c31 100644 --- a/package.json +++ b/package.json @@ -2,5 +2,8 @@ "devDependencies": { "prettier": "^3.1.1", "prettier-plugin-solidity": "^1.3.1" + }, + "dependencies": { + "ethereum-private-key-to-public-key": "^0.0.5" } } From edbf7f6e56f39c5ba0fe03ca154017c0b0c9604d Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 7 Feb 2024 14:33:06 +0800 Subject: [PATCH 04/10] precom fix --- auction-server/src/api/marketplace.rs | 152 ++++++++++++---------- auction-server/src/liquidation_adapter.rs | 4 +- per_sdk/protocols/token_vault_monitor.py | 27 +++- 3 files changed, 109 insertions(+), 74 deletions(-) diff --git a/auction-server/src/api/marketplace.rs b/auction-server/src/api/marketplace.rs index 8ca4b727..9cba7935 100644 --- a/auction-server/src/api/marketplace.rs +++ b/auction-server/src/api/marketplace.rs @@ -140,14 +140,14 @@ pub async fn submit_opportunity( .as_secs() as UnixTimestamp, chain_id: opportunity.chain_id.clone(), permission_key: opportunity.permission_key.clone(), - contract: opportunity.contract, - calldata: opportunity.calldata, - value: opportunity.value, - repay_tokens, - receipt_tokens, + contract: opportunity.contract.clone(), + calldata: opportunity.calldata.clone(), + value: opportunity.value.clone(), + repay_tokens: repay_tokens.clone(), + receipt_tokens: receipt_tokens.clone(), bidders: Default::default(), }; - + verify_opportunity( verified_opportunity.clone(), chain_store, @@ -191,6 +191,14 @@ pub async fn submit_opportunity( .await .insert(opportunity.permission_key.clone(), opportunities_existing); + tracing::info!( + "number of permission keys: {}", + store.liquidation_store.opportunities.read().await.len() + ); + tracing::info!( + "number of opportunities for key: {}", + store.liquidation_store.opportunities.read().await[&opportunity.permission_key].len() + ); Ok(id.to_string()) } @@ -202,7 +210,7 @@ pub async fn submit_opportunity( pub async fn fetch_opportunities( State(store): State>, ) -> Result>, RestError> { - let opportunities: Vec = store + let opportunity: Vec = store .liquidation_store .opportunities .read() @@ -210,20 +218,23 @@ pub async fn fetch_opportunities( .values() .cloned() .map(|opportunities| LiquidationOpportunityWithId { + // only expose the most recent opportunity opportunity_id: opportunities[0].id, opportunity: LiquidationOpportunity { - permission_key: opportunities[0].permission_key, - chain_id: opportunities[0].chain_id, + permission_key: opportunities[0].permission_key.clone(), + chain_id: opportunities[0].chain_id.clone(), contract: opportunities[0].contract, - calldata: opportunities[0].calldata, + calldata: opportunities[0].calldata.clone(), value: opportunities[0].value, repay_tokens: opportunities[0] .repay_tokens + .clone() .into_iter() .map(TokenQty::from) .collect(), receipt_tokens: opportunities[0] .receipt_tokens + .clone() .into_iter() .map(TokenQty::from) .collect(), @@ -231,7 +242,7 @@ pub async fn fetch_opportunities( }) .collect(); - Ok(opportunities.into()) + Ok(opportunity.into()) } #[derive(Serialize, Deserialize, ToSchema, Clone)] @@ -270,7 +281,7 @@ pub async fn bid_opportunity( State(store): State>, Json(opportunity_bid): Json, ) -> Result { - let liquidation = store + let opportunities_liquidation = store .liquidation_store .opportunities .read() @@ -279,70 +290,71 @@ pub async fn bid_opportunity( .ok_or(RestError::OpportunityNotFound)? .clone(); + let position_id = opportunities_liquidation + .iter() + .position(|o| o.id == opportunity_bid.opportunity_id); - // TODO: delete these prints - tracing::info!("Opportunity: {:?}", liquidation.id); - tracing::info!("Opp bid: {:?}", opportunity_bid.opportunity_id); - - // this check fails whenever opportunity ID is updated by the protocol monitor, which it can be often - if liquidation.id != opportunity_bid.opportunity_id { - return Err(RestError::BadParameters( - "Invalid opportunity_id".to_string(), - )); - } + match position_id { + Some(index) => { + let liquidation = opportunities_liquidation[index].clone(); - // TODO: move this logic to searcher side - if liquidation.bidders.contains(&opportunity_bid.liquidator) { - return Err(RestError::BadParameters( - "Liquidator already bid on this opportunity".to_string(), - )); - } + // TODO: move this logic to searcher side + if liquidation.bidders.contains(&opportunity_bid.liquidator) { + return Err(RestError::BadParameters( + "Liquidator already bid on this opportunity".to_string(), + )); + } - let chain_store = store - .chains - .get(&liquidation.chain_id) - .ok_or(RestError::InvalidChainId)?; + let chain_store = store + .chains + .get(&liquidation.chain_id) + .ok_or(RestError::InvalidChainId)?; - let per_calldata = make_liquidator_calldata( - liquidation.clone(), - opportunity_bid.clone(), - chain_store.provider.clone(), - chain_store.config.adapter_contract, - ) - .await - .map_err(|e| RestError::BadParameters(e.to_string()))?; - match handle_bid( - store.clone(), - crate::api::rest::Bid { - permission_key: liquidation.permission_key.clone(), - chain_id: liquidation.chain_id.clone(), - contract: chain_store.config.adapter_contract, - calldata: per_calldata, - amount: opportunity_bid.amount, - }, - ) - .await - { - Ok(_) => { - let mut write_guard = store.liquidation_store.opportunities.write().await; - let liquidation = write_guard.get_mut(&opportunity_bid.permission_key); - if let Some(liquidation) = liquidation { - liquidation.bidders.insert(opportunity_bid.liquidator); - } - Ok("OK".to_string()) - } - Err(e) => match e { - RestError::SimulationError { result, reason } => { - let parsed = parse_revert_error(&result); - match parsed { - Some(decoded) => Err(RestError::BadParameters(decoded)), - None => { - tracing::info!("Could not parse revert reason: {}", reason); - Err(RestError::SimulationError { result, reason }) + let per_calldata = make_liquidator_calldata( + liquidation.clone(), + opportunity_bid.clone(), + chain_store.provider.clone(), + chain_store.config.adapter_contract, + ) + .await + .map_err(|e| RestError::BadParameters(e.to_string()))?; + match handle_bid( + store.clone(), + crate::api::rest::Bid { + permission_key: liquidation.permission_key.clone(), + chain_id: liquidation.chain_id.clone(), + contract: chain_store.config.adapter_contract, + calldata: per_calldata, + amount: opportunity_bid.amount, + }, + ) + .await + { + Ok(_) => { + let mut write_guard = store.liquidation_store.opportunities.write().await; + let liquidation = write_guard.get_mut(&opportunity_bid.permission_key); + if let Some(liquidation) = liquidation { + liquidation[index] + .bidders + .insert(opportunity_bid.liquidator); } + Ok("OK".to_string()) } + Err(e) => match e { + RestError::SimulationError { result, reason } => { + let parsed = parse_revert_error(&result); + match parsed { + Some(decoded) => Err(RestError::BadParameters(decoded)), + None => { + tracing::info!("Could not parse revert reason: {}", reason); + Err(RestError::SimulationError { result, reason }) + } + } + } + _ => Err(e), + }, } - _ => Err(e), - }, + } + None => Err(RestError::OpportunityNotFound), } } diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index c64e1c54..8e84de0a 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -333,7 +333,9 @@ pub async fn run_verification_loop(store: Arc) { while !SHOULD_EXIT.load(Ordering::Acquire) { let all_opportunities = store.liquidation_store.opportunities.read().await.clone(); for (permission_key, opportunity) in all_opportunities.iter() { - match verify_with_store(opportunity.clone(), &store).await { + // just need to check the most recent opportunity, if that fails the rest should also be removed + // TODO: this is true for subsequent opportunities that only have updated price updates, but may not be true generally; we should think about how best to do this (one option is to just check every single saved opportunity and remove from the store one by one) + match verify_with_store(opportunity[0].clone(), &store).await { Ok(_) => {} Err(e) => { store diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index 6e9e866e..e938ae7e 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -43,12 +43,18 @@ def get_vault_abi(): class VaultMonitor: def __init__( - self, rpc_url: str, contract_address: str, weth_address: str, chain_id: str + self, + rpc_url: str, + contract_address: str, + weth_address: str, + chain_id: str, + mock_pyth: bool, ): self.rpc_url = rpc_url self.contract_address = contract_address self.weth_address = weth_address self.chain_id = chain_id + self.mock_pyth = mock_pyth self.w3 = web3.AsyncWeb3(web3.AsyncHTTPProvider(rpc_url)) self.token_vault = self.w3.eth.contract( @@ -115,7 +121,11 @@ def create_liquidation_opp( A LiquidationOpportunity object corresponding to the specified account. """ - price_updates = [base64.b64decode(update["vaa"]) for update in prices] + if self.mock_pyth: + # TODO: do we want to update with mock pyth prices from the vaas? + price_updates = [] + else: + price_updates = [base64.b64decode(update["vaa"]) for update in prices] calldata = self.token_vault.encodeABI( fn_name="liquidateWithPriceUpdate", @@ -243,6 +253,13 @@ async def main(): dest="weth_contract", help="WETH contract address", ) + parser.add_argument( + "--mock-pyth", + action="store_true", + dest="mock_pyth", + default=False, + help="If provided, will not include price update VAAs in the on-chain submission because MockPyth is being used", + ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--dry-run", @@ -267,7 +284,11 @@ async def main(): logger.addHandler(log_handler) monitor = VaultMonitor( - args.rpc_url, args.vault_contract, args.weth_contract, args.chain_id + args.rpc_url, + args.vault_contract, + args.weth_contract, + args.chain_id, + args.mock_pyth, ) while True: From 335682d4253c16f673520b1d7ab249ede4aa2182 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 7 Feb 2024 17:14:19 +0800 Subject: [PATCH 05/10] clearer error raising on smart contracts --- per_multicall/src/Errors.sol | 15 +++++++++++ per_multicall/src/LiquidationAdapter.sol | 7 +++-- per_multicall/src/PERMulticall.sol | 33 ++++++++++++------------ per_multicall/src/TokenVault.sol | 15 ++++++----- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/per_multicall/src/Errors.sol b/per_multicall/src/Errors.sol index ca8a0606..f4e76d0f 100644 --- a/per_multicall/src/Errors.sol +++ b/per_multicall/src/Errors.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.13; // Signature: 0x82b42900 error Unauthorized(); +// Signature: 0x868a64de +error InvalidPermission(); + // Signature: 0xf136a5b7 error InvalidSearcherSignature(); @@ -30,3 +33,15 @@ error NotRegistered(); // Signature: 0xd3c8346c error LiquidationCallFailed(string reason); + +// Signature: 0x4af147aa +error InsufficientTokenReceived(); + +// Signature: 0x61ca76d2 +error NegativePrice(); + +// Signature: 0x4a7a3163 +error InvalidHealthRatios(); + +// Signature: 0x4be6321b +error InvalidSignatureLength(); diff --git a/per_multicall/src/LiquidationAdapter.sol b/per_multicall/src/LiquidationAdapter.sol index b785f081..b44a9dad 100644 --- a/per_multicall/src/LiquidationAdapter.sol +++ b/per_multicall/src/LiquidationAdapter.sol @@ -144,10 +144,9 @@ contract LiquidationAdapter is SigVerify { uint256 amount = params.expectedReceiptTokens[i].amount; uint256 balanceFinal = token.balanceOf(address(this)); - require( - balanceFinal >= balancesExpectedReceipt[i], - "insufficient token received" - ); + if (balanceFinal < balancesExpectedReceipt[i]) { + revert InsufficientTokenReceived(); + } // transfer receipt tokens to liquidator token.transfer(params.liquidator, amount); diff --git a/per_multicall/src/PERMulticall.sol b/per_multicall/src/PERMulticall.sol index 29196e52..f55b3c67 100644 --- a/per_multicall/src/PERMulticall.sol +++ b/per_multicall/src/PERMulticall.sol @@ -49,10 +49,9 @@ contract PERMulticall { * @param feeSplit: amount of fee to be split with the protocol. 10**18 is 100% */ function setFee(address feeRecipient, uint256 feeSplit) public { - require( - msg.sender == _perOperator, - "only PER operator can set the fees" - ); + if (msg.sender != _perOperator) { + revert Unauthorized(); + } _feeConfig[feeRecipient] = feeSplit; } @@ -84,14 +83,13 @@ contract PERMulticall { bytes[] calldata data, uint256[] calldata bids ) public payable returns (MulticallStatus[] memory multicallStatuses) { - require( - msg.sender == _perOperator, - "only PER operator can call this function" - ); - require( - permission.length >= 20, - "permission size should be at least 20 bytes" - ); + if (msg.sender != _perOperator) { + revert Unauthorized(); + } + if (permission.length < 20) { + revert InvalidPermission(); + } + _permissions[keccak256(permission)] = true; multicallStatuses = new MulticallStatus[](data.length); @@ -152,11 +150,12 @@ contract PERMulticall { uint256 balanceFinalEth = address(this).balance; // ensure that PER operator was paid at least bid ETH - require( - !(balanceFinalEth - balanceInitEth < bid) && - !(balanceFinalEth < balanceInitEth), - "invalid bid" - ); + if ( + (balanceFinalEth - balanceInitEth < bid) || + (balanceFinalEth < balanceInitEth) + ) { + revert InvalidBid(); + } } return (success, result); diff --git a/per_multicall/src/TokenVault.sol b/per_multicall/src/TokenVault.sol index 69d18422..db7521ed 100644 --- a/per_multicall/src/TokenVault.sol +++ b/per_multicall/src/TokenVault.sol @@ -95,8 +95,12 @@ contract TokenVault is PERFeeReceiver { uint256 priceCollateral = _getPrice(vault.tokenIDCollateral); uint256 priceDebt = _getPrice(vault.tokenIDDebt); - require(priceCollateral >= 0, "collateral price is negative"); - require(priceDebt >= 0, "debt price is negative"); + if (priceCollateral < 0) { + revert NegativePrice(); + } + if (priceDebt < 0) { + revert NegativePrice(); + } uint256 valueCollateral = priceCollateral * vault.amountCollateral; uint256 valueDebt = priceDebt * vault.amountDebt; @@ -139,10 +143,9 @@ contract TokenVault is PERFeeReceiver { tokenIDCollateral, tokenIDDebt ); - require( - minPermissionLessHealthRatio <= minHealthRatio, - "minPermissionLessHealthRatio must be less than or equal to minHealthRatio" - ); + if (minPermissionLessHealthRatio > minHealthRatio) { + revert InvalidHealthRatios(); + } if (_getVaultHealth(vault) < vault.minHealthRatio) { revert UncollateralizedVaultCreation(); } From 1b562ecfe35e7ecb35dafdbcd2ba369f59b2fed9 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Wed, 7 Feb 2024 17:54:09 +0800 Subject: [PATCH 06/10] price updates in mock pyth format --- per_sdk/protocols/token_vault_monitor.py | 41 ++++++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index e938ae7e..c9d90d22 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -48,12 +48,14 @@ def __init__( contract_address: str, weth_address: str, chain_id: str, + include_price_updates: bool, mock_pyth: bool, ): self.rpc_url = rpc_url self.contract_address = contract_address self.weth_address = weth_address self.chain_id = chain_id + self.include_price_updates = include_price_updates self.mock_pyth = mock_pyth self.w3 = web3.AsyncWeb3(web3.AsyncHTTPProvider(rpc_url)) @@ -120,12 +122,29 @@ def create_liquidation_opp( Returns: A LiquidationOpportunity object corresponding to the specified account. """ - - if self.mock_pyth: - # TODO: do we want to update with mock pyth prices from the vaas? - price_updates = [] - else: - price_updates = [base64.b64decode(update["vaa"]) for update in prices] + price_updates = [] + + if self.include_price_updates: + if self.mock_pyth: + price_updates = [] + + for update in prices: + feed_id = bytes.fromhex(update["feed_id"]) + price = tuple([int(x) for x in update["price"].values()]) + price_ema = tuple([int(x) for x in update["price_ema"].values()]) + price_updates.append( + encode( + [ + "bytes32", + "(int64,uint64,int32,uint64)", + "(int64,uint64,int32,uint64)", + "uint64", + ], + [feed_id, price, price_ema, 0], + ) + ) + else: + price_updates = [base64.b64decode(update["vaa"]) for update in prices] calldata = self.token_vault.encodeABI( fn_name="liquidateWithPriceUpdate", @@ -253,12 +272,19 @@ async def main(): dest="weth_contract", help="WETH contract address", ) + parser.add_argument( + "--exclude-price-updates", + action="store_false", + dest="include_price_updates", + default=True, + help="If provided, will exclude Pyth price updates from the liquidation call. Should only be used in testing.", + ) parser.add_argument( "--mock-pyth", action="store_true", dest="mock_pyth", default=False, - help="If provided, will not include price update VAAs in the on-chain submission because MockPyth is being used", + help="If provided, will construct price updates in MockPyth format rather than VAAs", ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( @@ -288,6 +314,7 @@ async def main(): args.vault_contract, args.weth_contract, args.chain_id, + args.include_price_updates, args.mock_pyth, ) From 559e153d67b4cbb8dca8d683a54557ab889ef2d5 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 8 Feb 2024 17:03:45 +0800 Subject: [PATCH 07/10] address comments --- auction-server/src/api/marketplace.rs | 276 +++++++++++----------- auction-server/src/liquidation_adapter.rs | 48 ++-- auction-server/src/state.rs | 2 +- package.json | 3 - per_multicall/remappings.txt | 1 - per_multicall/script/Vault.s.sol | 9 +- per_multicall/src/Errors.sol | 6 - per_multicall/src/TokenVaultErrors.sol | 10 + per_sdk/protocols/token_vault_monitor.py | 9 +- per_sdk/searcher/simple_searcher.py | 7 +- per_sdk/utils/pyth_prices.py | 9 + 11 files changed, 206 insertions(+), 174 deletions(-) diff --git a/auction-server/src/api/marketplace.rs b/auction-server/src/api/marketplace.rs index 9cba7935..07d5af3e 100644 --- a/auction-server/src/api/marketplace.rs +++ b/auction-server/src/api/marketplace.rs @@ -92,6 +92,73 @@ pub struct LiquidationOpportunityWithId { opportunity: LiquidationOpportunity, } +impl PartialEq for VerifiedLiquidationOpportunity { + fn eq(&self, other: &LiquidationOpportunity) -> bool { + self.chain_id == other.chain_id + && self.contract == other.contract + && self.calldata == other.calldata + && self.value == other.value + && self.repay_tokens == parse_tokens(other.repay_tokens.clone()) + && self.receipt_tokens == parse_tokens(other.receipt_tokens.clone()) + } +} + +impl PartialEq for LiquidationOpportunity { + fn eq(&self, other: &VerifiedLiquidationOpportunity) -> bool { + self.chain_id == other.chain_id + && self.contract == other.contract + && self.calldata == other.calldata + && self.value == other.value + && parse_tokens(self.repay_tokens.clone()) == other.repay_tokens + && parse_tokens(self.receipt_tokens.clone()) == other.receipt_tokens + } +} + +impl Into for VerifiedLiquidationOpportunity { + fn into(self) -> LiquidationOpportunityWithId { + LiquidationOpportunityWithId { + opportunity_id: self.id, + opportunity: LiquidationOpportunity { + permission_key: self.permission_key, + chain_id: self.chain_id, + contract: self.contract, + calldata: self.calldata, + value: self.value, + repay_tokens: self.repay_tokens.into_iter().map(TokenQty::from).collect(), + receipt_tokens: self + .receipt_tokens + .into_iter() + .map(TokenQty::from) + .collect(), + }, + } + } +} + +impl Into for LiquidationOpportunity { + fn into(self) -> VerifiedLiquidationOpportunity { + let id = Uuid::new_v4(); + let creation_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| RestError::BadParameters("Invalid system time".to_string())) + // TODO: this may not be the best way to handle this + .unwrap_or_default() + .as_secs() as UnixTimestamp; + VerifiedLiquidationOpportunity { + id, + creation_time, + chain_id: self.chain_id, + permission_key: self.permission_key, + contract: self.contract, + calldata: self.calldata, + value: self.value, + repay_tokens: parse_tokens(self.repay_tokens), + receipt_tokens: parse_tokens(self.receipt_tokens), + bidders: Default::default(), + } + } +} + impl From<(Address, U256)> for TokenQty { fn from(token: (Address, U256)) -> Self { TokenQty { @@ -128,25 +195,7 @@ pub async fn submit_opportunity( .get(&opportunity.chain_id) .ok_or(RestError::InvalidChainId)?; - let repay_tokens = parse_tokens(opportunity.repay_tokens); - let receipt_tokens = parse_tokens(opportunity.receipt_tokens); - - let id = Uuid::new_v4(); - let verified_opportunity = VerifiedLiquidationOpportunity { - id, - creation_time: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| RestError::BadParameters("Invalid system time".to_string()))? - .as_secs() as UnixTimestamp, - chain_id: opportunity.chain_id.clone(), - permission_key: opportunity.permission_key.clone(), - contract: opportunity.contract.clone(), - calldata: opportunity.calldata.clone(), - value: opportunity.value.clone(), - repay_tokens: repay_tokens.clone(), - receipt_tokens: receipt_tokens.clone(), - bidders: Default::default(), - }; + let verified_opportunity: VerifiedLiquidationOpportunity = opportunity.clone().into(); verify_opportunity( verified_opportunity.clone(), @@ -156,50 +205,33 @@ pub async fn submit_opportunity( .await .map_err(|e| RestError::InvalidOpportunity(e.to_string()))?; - let mut opportunities_existing = Vec::new(); + let mut write_lock = store.liquidation_store.opportunities.write().await; - if store - .liquidation_store - .opportunities - .read() - .await - .contains_key(&opportunity.permission_key) - { - opportunities_existing = - store.liquidation_store.opportunities.read().await[&opportunity.permission_key].clone(); - let opportunity_top = opportunities_existing[0].clone(); + if write_lock.contains_key(&opportunity.permission_key) { + let opportunities_existing = write_lock[&opportunity.permission_key].clone(); + let opportunity_top = opportunities_existing[opportunities_existing.len() - 1].clone(); // check if exact same opportunity exists already - if opportunity_top.chain_id == opportunity.chain_id - && opportunity_top.contract == opportunity.contract - && opportunity_top.calldata == opportunity.calldata - && opportunity_top.value == opportunity.value - && opportunity_top.repay_tokens == repay_tokens - && opportunity_top.receipt_tokens == receipt_tokens - { + if opportunity_top == opportunity { return Err(RestError::BadParameters( "Duplicate opportunity submission".to_string(), )); } } - opportunities_existing.push(verified_opportunity.clone()); - - store - .liquidation_store - .opportunities - .write() - .await - .insert(opportunity.permission_key.clone(), opportunities_existing); + if let Some(x) = write_lock.get_mut(&opportunity.permission_key) { + x.push(verified_opportunity.clone()); + } else { + let mut opportunities = Vec::new(); + opportunities.push(verified_opportunity.clone()); + write_lock.insert(verified_opportunity.permission_key.clone(), opportunities); + } - tracing::info!( - "number of permission keys: {}", - store.liquidation_store.opportunities.read().await.len() - ); - tracing::info!( + tracing::debug!("number of permission keys: {}", write_lock.len()); + tracing::debug!( "number of opportunities for key: {}", - store.liquidation_store.opportunities.read().await[&opportunity.permission_key].len() + write_lock[&opportunity.permission_key].len() ); - Ok(id.to_string()) + Ok(verified_opportunity.id.to_string()) } /// Fetch all liquidation opportunities ready to be exectued. @@ -210,39 +242,21 @@ pub async fn submit_opportunity( pub async fn fetch_opportunities( State(store): State>, ) -> Result>, RestError> { - let opportunity: Vec = store + let opportunities: Vec = store .liquidation_store .opportunities .read() .await .values() .cloned() - .map(|opportunities| LiquidationOpportunityWithId { - // only expose the most recent opportunity - opportunity_id: opportunities[0].id, - opportunity: LiquidationOpportunity { - permission_key: opportunities[0].permission_key.clone(), - chain_id: opportunities[0].chain_id.clone(), - contract: opportunities[0].contract, - calldata: opportunities[0].calldata.clone(), - value: opportunities[0].value, - repay_tokens: opportunities[0] - .repay_tokens - .clone() - .into_iter() - .map(TokenQty::from) - .collect(), - receipt_tokens: opportunities[0] - .receipt_tokens - .clone() - .into_iter() - .map(TokenQty::from) - .collect(), - }, + .map(|opportunities_key| { + opportunities_key[opportunities_key.len() - 1] + .clone() + .into() }) .collect(); - Ok(opportunity.into()) + Ok(opportunities.into()) } #[derive(Serialize, Deserialize, ToSchema, Clone)] @@ -292,69 +306,65 @@ pub async fn bid_opportunity( let position_id = opportunities_liquidation .iter() - .position(|o| o.id == opportunity_bid.opportunity_id); + .position(|o| o.id == opportunity_bid.opportunity_id) + .ok_or(RestError::OpportunityNotFound)?; - match position_id { - Some(index) => { - let liquidation = opportunities_liquidation[index].clone(); + let liquidation = opportunities_liquidation[position_id].clone(); - // TODO: move this logic to searcher side - if liquidation.bidders.contains(&opportunity_bid.liquidator) { - return Err(RestError::BadParameters( - "Liquidator already bid on this opportunity".to_string(), - )); - } + // TODO: move this logic to searcher side + if liquidation.bidders.contains(&opportunity_bid.liquidator) { + return Err(RestError::BadParameters( + "Liquidator already bid on this opportunity".to_string(), + )); + } - let chain_store = store - .chains - .get(&liquidation.chain_id) - .ok_or(RestError::InvalidChainId)?; + let chain_store = store + .chains + .get(&liquidation.chain_id) + .ok_or(RestError::InvalidChainId)?; - let per_calldata = make_liquidator_calldata( - liquidation.clone(), - opportunity_bid.clone(), - chain_store.provider.clone(), - chain_store.config.adapter_contract, - ) - .await - .map_err(|e| RestError::BadParameters(e.to_string()))?; - match handle_bid( - store.clone(), - crate::api::rest::Bid { - permission_key: liquidation.permission_key.clone(), - chain_id: liquidation.chain_id.clone(), - contract: chain_store.config.adapter_contract, - calldata: per_calldata, - amount: opportunity_bid.amount, - }, - ) - .await - { - Ok(_) => { - let mut write_guard = store.liquidation_store.opportunities.write().await; - let liquidation = write_guard.get_mut(&opportunity_bid.permission_key); - if let Some(liquidation) = liquidation { - liquidation[index] - .bidders - .insert(opportunity_bid.liquidator); + let per_calldata = make_liquidator_calldata( + liquidation.clone(), + opportunity_bid.clone(), + chain_store.provider.clone(), + chain_store.config.adapter_contract, + ) + .await + .map_err(|e| RestError::BadParameters(e.to_string()))?; + match handle_bid( + store.clone(), + crate::api::rest::Bid { + permission_key: liquidation.permission_key.clone(), + chain_id: liquidation.chain_id.clone(), + contract: chain_store.config.adapter_contract, + calldata: per_calldata, + amount: opportunity_bid.amount, + }, + ) + .await + { + Ok(_) => { + let mut write_guard = store.liquidation_store.opportunities.write().await; + let liquidation = write_guard.get_mut(&opportunity_bid.permission_key); + if let Some(liquidation) = liquidation { + liquidation[position_id] + .bidders + .insert(opportunity_bid.liquidator); + } + Ok("OK".to_string()) + } + Err(e) => match e { + RestError::SimulationError { result, reason } => { + let parsed = parse_revert_error(&result); + match parsed { + Some(decoded) => Err(RestError::BadParameters(decoded)), + None => { + tracing::info!("Could not parse revert reason: {}", reason); + Err(RestError::SimulationError { result, reason }) } - Ok("OK".to_string()) } - Err(e) => match e { - RestError::SimulationError { result, reason } => { - let parsed = parse_revert_error(&result); - match parsed { - Some(decoded) => Err(RestError::BadParameters(decoded)), - None => { - tracing::info!("Could not parse revert reason: {}", reason); - Err(RestError::SimulationError { result, reason }) - } - } - } - _ => Err(e), - }, } - } - None => Err(RestError::OpportunityNotFound), + _ => Err(e), + }, } } diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 8e84de0a..e6b836fa 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -328,27 +328,45 @@ async fn verify_with_store( /// # Arguments /// /// * `store`: server store -pub async fn run_verification_loop(store: Arc) { +pub async fn run_verification_loop(store: Arc) -> Result<()> { tracing::info!("Starting opportunity verifier..."); while !SHOULD_EXIT.load(Ordering::Acquire) { - let all_opportunities = store.liquidation_store.opportunities.read().await.clone(); - for (permission_key, opportunity) in all_opportunities.iter() { - // just need to check the most recent opportunity, if that fails the rest should also be removed - // TODO: this is true for subsequent opportunities that only have updated price updates, but may not be true generally; we should think about how best to do this (one option is to just check every single saved opportunity and remove from the store one by one) - match verify_with_store(opportunity[0].clone(), &store).await { - Ok(_) => {} - Err(e) => { - store - .liquidation_store - .opportunities - .write() - .await - .remove(permission_key); - tracing::info!("Removed Opportunity with failed verification: {}", e); + // set write lock early to prevent keys from being removed while we are iterating over them + // TODO: is this the best place to initiate the lock? + let mut write_lock = store.liquidation_store.opportunities.write().await; + let all_opportunities = write_lock.clone(); + for (permission_key, opportunities) in all_opportunities.iter() { + // check each of the opportunities for this permission key for validity + let mut inds_to_remove = vec![]; + let mut ind = 0; + while ind < opportunities.len() { + match verify_with_store(opportunities[ind].clone(), &store).await { + Ok(_) => {} + Err(e) => { + inds_to_remove.push(ind); + tracing::info!("Removing Opportunity with failed verification: {}", e); + } } + ind += 1 + } + + for ind in inds_to_remove.iter().rev() { + write_lock + .get_mut(permission_key) + .ok_or(anyhow!("Permission key not found"))? + .remove(*ind); + } + + if write_lock + .get(permission_key) + .ok_or(anyhow!("Permission key not found"))? + .is_empty() + { + write_lock.remove(permission_key); } } tokio::time::sleep(Duration::from_secs(5)).await; // this should be replaced by a subscription to the chain and trigger on new blocks } tracing::info!("Shutting down opportunity verifier..."); + Ok(()) } diff --git a/auction-server/src/state.rs b/auction-server/src/state.rs index 3eb4b637..1c9c7eee 100644 --- a/auction-server/src/state.rs +++ b/auction-server/src/state.rs @@ -34,7 +34,7 @@ pub struct SimulatedBid { } pub type UnixTimestamp = i64; -#[derive(Clone)] +#[derive(Clone, PartialEq, Debug)] pub struct VerifiedLiquidationOpportunity { pub id: Uuid, pub creation_time: UnixTimestamp, diff --git a/package.json b/package.json index 145b1c31..19654a1f 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,5 @@ "devDependencies": { "prettier": "^3.1.1", "prettier-plugin-solidity": "^1.3.1" - }, - "dependencies": { - "ethereum-private-key-to-public-key": "^0.0.5" } } diff --git a/per_multicall/remappings.txt b/per_multicall/remappings.txt index c877de03..821595eb 100644 --- a/per_multicall/remappings.txt +++ b/per_multicall/remappings.txt @@ -4,4 +4,3 @@ erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ forge-std/=lib/forge-std/src/ openzeppelin-contracts/=lib/openzeppelin-contracts/ @pythnetwork/pyth-sdk-solidity=node_modules/@pythnetwork/pyth-sdk-solidity/ -solidity-bytes-utils=node_modules/solidity-bytes-utils diff --git a/per_multicall/script/Vault.s.sol b/per_multicall/script/Vault.s.sol index ee92c294..ce224a0a 100644 --- a/per_multicall/script/Vault.s.sol +++ b/per_multicall/script/Vault.s.sol @@ -248,8 +248,8 @@ contract VaultScript is Script { console.logBytes32(idToken1); console.logBytes32(idToken2); - token1 = new MyToken(idToken1Str, "T1"); - token2 = new MyToken(idToken2Str, "T2"); + token1 = new MyToken(idToken1Str, "T_ETH"); + token2 = new MyToken(idToken2Str, "T_BTC"); console.log("token 1 address", address(token1)); console.log("token 2 address", address(token2)); @@ -413,11 +413,6 @@ contract VaultScript is Script { IERC20(token2Latest).balanceOf(depositorLatest) ); - address oracleLatest = vm.parseJsonAddress(json, ".oracle"); - MockPyth oracle = MockPyth(payable(oracleLatest)); - console.log(oracleLatest); - console.logInt(oracle.queryPriceFeed(idToken1Latest).price.price); - if (collatT1) { vm.startBroadcast(depositorSkLatest); IERC20(token1Latest).approve(address(tokenVaultLatest), qT1); diff --git a/per_multicall/src/Errors.sol b/per_multicall/src/Errors.sol index f4e76d0f..578b18e2 100644 --- a/per_multicall/src/Errors.sol +++ b/per_multicall/src/Errors.sol @@ -37,11 +37,5 @@ error LiquidationCallFailed(string reason); // Signature: 0x4af147aa error InsufficientTokenReceived(); -// Signature: 0x61ca76d2 -error NegativePrice(); - -// Signature: 0x4a7a3163 -error InvalidHealthRatios(); - // Signature: 0x4be6321b error InvalidSignatureLength(); diff --git a/per_multicall/src/TokenVaultErrors.sol b/per_multicall/src/TokenVaultErrors.sol index 7898adad..06e4540f 100644 --- a/per_multicall/src/TokenVaultErrors.sol +++ b/per_multicall/src/TokenVaultErrors.sol @@ -1,10 +1,20 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; +// Signature: 0xe922edfd error UncollateralizedVaultCreation(); +// Signature: 0xdcb430ee error InvalidVaultUpdate(); +// Signature: 0x9cd7b1c6 error InvalidPriceExponent(); +// Signature: 0x85914873 error InvalidLiquidation(); + +// Signature: 0x61ca76d2 +error NegativePrice(); + +// Signature: 0x4a7a3163 +error InvalidHealthRatios(); diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index c9d90d22..b90742f3 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -9,7 +9,7 @@ import web3 from eth_abi import encode -from per_sdk.utils.pyth_prices import PriceFeed, PriceFeedClient +from per_sdk.utils.pyth_prices import PriceFeed, PriceFeedClient, price_to_tuple from per_sdk.utils.types_liquidation_adapter import LiquidationOpportunity logger = logging.getLogger(__name__) @@ -130,8 +130,9 @@ def create_liquidation_opp( for update in prices: feed_id = bytes.fromhex(update["feed_id"]) - price = tuple([int(x) for x in update["price"].values()]) - price_ema = tuple([int(x) for x in update["price_ema"].values()]) + price = price_to_tuple(update["price"]) + price_ema = price_to_tuple(update["price_ema"]) + prev_publish_time = 0 price_updates.append( encode( [ @@ -140,7 +141,7 @@ def create_liquidation_opp( "(int64,uint64,int32,uint64)", "uint64", ], - [feed_id, price, price_ema, 0], + [feed_id, price, price_ema, prev_publish_time], ) ) else: diff --git a/per_sdk/searcher/simple_searcher.py b/per_sdk/searcher/simple_searcher.py index 3ede95a2..c3247c74 100644 --- a/per_sdk/searcher/simple_searcher.py +++ b/per_sdk/searcher/simple_searcher.py @@ -154,11 +154,10 @@ async def main(): tx = create_liquidation_transaction( liquidation_opp, sk_liquidator, bid_info ) - import pdb - pdb.set_trace() - - resp = await client.post(LIQUIDATION_SERVER_ENDPOINT_BID, json=tx) + resp = await client.post( + LIQUIDATION_SERVER_ENDPOINT_BID, json=tx, timeout=20 + ) logger.info( "Submitted bid amount %s for opportunity %s, server response: %s", bid_info["bid"], diff --git a/per_sdk/utils/pyth_prices.py b/per_sdk/utils/pyth_prices.py index 92da18c2..d3727eb2 100644 --- a/per_sdk/utils/pyth_prices.py +++ b/per_sdk/utils/pyth_prices.py @@ -21,6 +21,15 @@ class PriceFeed(TypedDict): vaa: str +def price_to_tuple(price: Price): + return ( + int(price["price"]), + int(price["conf"]), + int(price["expo"]), + int(price["publish_time"]), + ) + + async def get_price_feed_ids() -> list[str]: """ Queries the Hermes https endpoint for a list of the IDs of all Pyth price feeds. From 5c5b21def6285943d354f85c26f7e29b21465b59 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Thu, 8 Feb 2024 19:27:20 +0800 Subject: [PATCH 08/10] fix poetry version mismatch --- per_sdk/protocols/token_vault_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/per_sdk/protocols/token_vault_monitor.py b/per_sdk/protocols/token_vault_monitor.py index 727f5197..2f80e284 100644 --- a/per_sdk/protocols/token_vault_monitor.py +++ b/per_sdk/protocols/token_vault_monitor.py @@ -238,7 +238,7 @@ async def get_liquidation_opportunities(self) -> list[LiquidationOpportunity]: logger.debug(f"Account {account['account_number']} health: {health}") if ( value_debt * int(account["min_health_ratio"]) - > value_collateral * 10 ** 18 + > value_collateral * 10**18 ): price_updates = [price_collateral, price_debt] liquidatable.append(self.create_liquidation_opp(account, price_updates)) From 7b2b62bb6ac6f378cedab64b7b729faf6b99c1a1 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Mon, 12 Feb 2024 15:34:10 -0500 Subject: [PATCH 09/10] changes to clones and write locks --- auction-server/src/api/liquidation.rs | 43 ++++++++++++----------- auction-server/src/liquidation_adapter.rs | 31 +++++++++------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index caa6d7c6..d7c2792b 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -118,22 +118,21 @@ pub async fn post_opportunity( let mut write_lock = store.liquidation_store.opportunities.write().await; if write_lock.contains_key(¶ms.permission_key) { - let opportunities_existing = write_lock[¶ms.permission_key].clone(); - let opportunity_top = opportunities_existing[opportunities_existing.len() - 1].clone(); - // check if exact same opportunity exists already - if opportunity_top == opportunity { - return Err(RestError::BadParameters( - "Duplicate opportunity submission".to_string(), - )); + let opportunities_existing = &write_lock[¶ms.permission_key]; + // check if same opportunity exists in the vector + for opportunity_existing in opportunities_existing { + if *opportunity_existing == opportunity { + return Err(RestError::BadParameters( + "Duplicate opportunity submission".to_string(), + )); + } } } if let Some(x) = write_lock.get_mut(¶ms.permission_key) { x.push(opportunity.clone()); } else { - let mut opportunities = Vec::new(); - opportunities.push(opportunity.clone()); - write_lock.insert(params.permission_key.clone(), opportunities); + write_lock.insert(params.permission_key.clone(), vec![opportunity]); } tracing::debug!("number of permission keys: {}", write_lock.len()); @@ -175,7 +174,9 @@ pub async fn get_opportunities( .values() .cloned() .map(|opportunities_key| { - opportunities_key[opportunities_key.len() - 1] + opportunities_key + .last() + .expect("A permission key vector should have at least one opportunity") .clone() .into() }) @@ -230,7 +231,7 @@ pub async fn post_bid( Path(opportunity_id): Path, Json(opportunity_bid): Json, ) -> Result, RestError> { - let opportunities_key = store + let opportunities = store .liquidation_store .opportunities .read() @@ -239,13 +240,11 @@ pub async fn post_bid( .ok_or(RestError::OpportunityNotFound)? .clone(); - let position_id = opportunities_key + let liquidation = opportunities .iter() - .position(|o| o.id == opportunity_id) + .find(|o| o.id == opportunity_id) .ok_or(RestError::OpportunityNotFound)?; - let liquidation = opportunities_key[position_id].clone(); - // TODO: move this logic to searcher side if liquidation.bidders.contains(&opportunity_bid.liquidator) { return Err(RestError::BadParameters( @@ -284,11 +283,13 @@ pub async fn post_bid( { Ok(_) => { let mut write_guard = store.liquidation_store.opportunities.write().await; - let liquidation = write_guard.get_mut(&opportunity_bid.permission_key); - if let Some(liquidation) = liquidation { - liquidation[position_id] - .bidders - .insert(opportunity_bid.liquidator); + let opportunities = write_guard.get_mut(&opportunity_bid.permission_key); + if let Some(opportunities) = opportunities { + let liquidation = opportunities + .iter_mut() + .find(|o| o.id == opportunity_id) + .ok_or(RestError::OpportunityNotFound)?; + liquidation.bidders.insert(opportunity_bid.liquidator); } Ok(BidResult { status: "OK".to_string(), diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 725e0113..108c692a 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -336,30 +336,32 @@ async fn verify_with_store(opportunity: LiquidationOpportunity, store: &Store) - pub async fn run_verification_loop(store: Arc) -> Result<()> { tracing::info!("Starting opportunity verifier..."); while !SHOULD_EXIT.load(Ordering::Acquire) { - // set write lock early to prevent keys from being removed while we are iterating over them - // TODO: is this the best place to initiate the lock? - let mut write_lock = store.liquidation_store.opportunities.write().await; - let all_opportunities = write_lock.clone(); + let all_opportunities = store.liquidation_store.opportunities.read().await.clone(); for (permission_key, opportunities) in all_opportunities.iter() { // check each of the opportunities for this permission key for validity - let mut inds_to_remove = vec![]; - let mut ind = 0; - while ind < opportunities.len() { - match verify_with_store(opportunities[ind].clone(), &store).await { + let mut opps_to_remove = vec![]; + for opportunity in opportunities.iter() { + match verify_with_store(opportunity.clone(), &store).await { Ok(_) => {} Err(e) => { - inds_to_remove.push(ind); - tracing::info!("Removing Opportunity with failed verification: {}", e); + opps_to_remove.push(opportunity.id); + tracing::info!( + "Removing Opportunity {} with failed verification: {}", + opportunity.id, + e + ); } } - ind += 1 } - for ind in inds_to_remove.iter().rev() { + // set write lock to remove all these opportunities + let mut write_lock = store.liquidation_store.opportunities.write().await; + + for id_opp in opps_to_remove { write_lock .get_mut(permission_key) .ok_or(anyhow!("Permission key not found"))? - .remove(*ind); + .retain(|x| x.id != id_opp); } if write_lock @@ -369,6 +371,9 @@ pub async fn run_verification_loop(store: Arc) -> Result<()> { { write_lock.remove(permission_key); } + + // release the write lock + drop(write_lock); } tokio::time::sleep(Duration::from_secs(5)).await; // this should be replaced by a subscription to the chain and trigger on new blocks } From 7a972056c566b49184241e45674ac847b88ce580 Mon Sep 17 00:00:00 2001 From: Anirudh Suresh Date: Tue, 13 Feb 2024 14:06:16 -0500 Subject: [PATCH 10/10] address comments --- auction-server/src/api/liquidation.rs | 21 +++++++++------------ auction-server/src/liquidation_adapter.rs | 18 +++++------------- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/auction-server/src/api/liquidation.rs b/auction-server/src/api/liquidation.rs index d7c2792b..f8945e29 100644 --- a/auction-server/src/api/liquidation.rs +++ b/auction-server/src/api/liquidation.rs @@ -117,20 +117,17 @@ pub async fn post_opportunity( let mut write_lock = store.liquidation_store.opportunities.write().await; - if write_lock.contains_key(¶ms.permission_key) { - let opportunities_existing = &write_lock[¶ms.permission_key]; + if let Some(opportunities_existing) = write_lock.get_mut(¶ms.permission_key) { // check if same opportunity exists in the vector - for opportunity_existing in opportunities_existing { - if *opportunity_existing == opportunity { + for opportunity_existing in opportunities_existing.clone() { + if opportunity_existing == opportunity { return Err(RestError::BadParameters( "Duplicate opportunity submission".to_string(), )); } } - } - if let Some(x) = write_lock.get_mut(¶ms.permission_key) { - x.push(opportunity.clone()); + opportunities_existing.push(opportunity); } else { write_lock.insert(params.permission_key.clone(), vec![opportunity]); } @@ -240,19 +237,19 @@ pub async fn post_bid( .ok_or(RestError::OpportunityNotFound)? .clone(); - let liquidation = opportunities + let opportunity = opportunities .iter() .find(|o| o.id == opportunity_id) .ok_or(RestError::OpportunityNotFound)?; // TODO: move this logic to searcher side - if liquidation.bidders.contains(&opportunity_bid.liquidator) { + if opportunity.bidders.contains(&opportunity_bid.liquidator) { return Err(RestError::BadParameters( "Liquidator already bid on this opportunity".to_string(), )); } - let params = match &liquidation.params { + let params = match &opportunity.params { OpportunityParams::V1(params) => params, }; @@ -285,11 +282,11 @@ pub async fn post_bid( let mut write_guard = store.liquidation_store.opportunities.write().await; let opportunities = write_guard.get_mut(&opportunity_bid.permission_key); if let Some(opportunities) = opportunities { - let liquidation = opportunities + let opportunity = opportunities .iter_mut() .find(|o| o.id == opportunity_id) .ok_or(RestError::OpportunityNotFound)?; - liquidation.bidders.insert(opportunity_bid.liquidator); + opportunity.bidders.insert(opportunity_bid.liquidator); } Ok(BidResult { status: "OK".to_string(), diff --git a/auction-server/src/liquidation_adapter.rs b/auction-server/src/liquidation_adapter.rs index 108c692a..71a9d6c4 100644 --- a/auction-server/src/liquidation_adapter.rs +++ b/auction-server/src/liquidation_adapter.rs @@ -357,19 +357,11 @@ pub async fn run_verification_loop(store: Arc) -> Result<()> { // set write lock to remove all these opportunities let mut write_lock = store.liquidation_store.opportunities.write().await; - for id_opp in opps_to_remove { - write_lock - .get_mut(permission_key) - .ok_or(anyhow!("Permission key not found"))? - .retain(|x| x.id != id_opp); - } - - if write_lock - .get(permission_key) - .ok_or(anyhow!("Permission key not found"))? - .is_empty() - { - write_lock.remove(permission_key); + if let Some(opportunities) = write_lock.get_mut(permission_key) { + opportunities.retain(|x| !opps_to_remove.contains(&x.id)); + if opportunities.is_empty() { + write_lock.remove(permission_key); + } } // release the write lock