From 4b79e6f20251c8069d582255afc07d853354a0ee Mon Sep 17 00:00:00 2001 From: sriyantra <85423375+sriyantra@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:49:11 -0700 Subject: [PATCH 1/2] feat: add recovery contracts --- EXTRACTION_PLAN.md | 49 +++++++++ contracts/recovery/AccountExtract.sol | 35 +++++++ contracts/recovery/LTokenExtract.sol | 38 +++++++ scripts/extractFunds.js | 137 ++++++++++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 EXTRACTION_PLAN.md create mode 100644 contracts/recovery/AccountExtract.sol create mode 100644 contracts/recovery/LTokenExtract.sol create mode 100644 scripts/extractFunds.js diff --git a/EXTRACTION_PLAN.md b/EXTRACTION_PLAN.md new file mode 100644 index 0000000..f9a3744 --- /dev/null +++ b/EXTRACTION_PLAN.md @@ -0,0 +1,49 @@ +# Sentiment V1 Protocol Extraction Plan + +## Overview +Extract all funds from deprecated Sentiment V1 protocol on Arbitrum to single recipient address. + +## Control Structure +- **ProxyAdmin**: `0x92f473Ef0Cd07080824F5e6B0859ac49b3AEb215` +- **ProxyAdmin Owner**: `0x3e5c63644E683549055b9be8653de26E0B4cd36e` +- **Controls**: 7 LTokens + Registry + AccountManager + Beacon (all user accounts) + +## Extraction Flow + +### Phase 1: Upgrade Contracts +1. **Upgrade 7 LToken proxies** to `LTokenExtract` implementation +2. **Upgrade Beacon** to `AccountExtract` implementation + +### Phase 2: Extract LToken Liquidity (~$23k) +Call `recoverFunds()` on each LToken: +- USDT: $634 +- USDC: $1,241 +- FRAX: $7,416 +- WETH: $13,196 +- WBTC: $16 +- ARB: $567 +- OHM: $6 + +### Phase 3: Extract Account Collateral (~$65k) +Call `recoverFunds()` on each of the 4,800+ accounts. + +## Key Functions + +**LTokenExtract.recoverFunds()** +- Extracts available liquidity only (totalAssets - borrows) +- Emits `Recovered(asset, amount)` event + +**AccountExtract.recoverFunds()** +- Loops through account's assets array +- Transfers each token balance to multisig +- Emits `Recovered(position, owner, asset, amount)` event per asset + +## Execution Script +Simple script that: +1. Calls `recoverFunds()` on 7 LTokens +2. Calls `recoverFunds()` on all accounts with assets +3. Handles batching for gas limits +4. Retries failed transactions + +## User Claims +After recovery, users query Dune for their `Recovered` events by owner address to see what funds they can claim from the multisig. \ No newline at end of file diff --git a/contracts/recovery/AccountExtract.sol b/contracts/recovery/AccountExtract.sol new file mode 100644 index 0000000..613604d --- /dev/null +++ b/contracts/recovery/AccountExtract.sol @@ -0,0 +1,35 @@ +// ABOUTME: Account implementation that adds fund extraction capability +// ABOUTME: Minimal addition to existing Account functionality for protocol deprecation + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "../../src/core/Account.sol"; +import "../../src/interface/core/IRegistry.sol"; +import "../../src/interface/core/IAccountManager.sol"; +import "../../src/interface/tokens/IERC20.sol"; + +contract AccountExtract is Account { + /// @notice Hardcoded multisig address to receive extracted funds + address constant MULTISIG = 0x000000000000000000000000000000000000dEaD; // TODO: Update with actual multisig + + event Recovered(address indexed position, address indexed owner, address indexed asset, uint256 amount); + + function recoverFunds() external { + // Get account owner from Registry + address owner = IRegistry(IAccountManager(accountManager).registry()).ownerFor(address(this)); + + // Loop through all assets and transfer them + for (uint i = 0; i < assets.length; i++) { + address asset = assets[i]; + if (asset != address(0)) { + IERC20 token = IERC20(asset); + uint256 balance = token.balanceOf(address(this)); + if (balance > 0) { + token.transfer(MULTISIG, balance); + emit Recovered(address(this), owner, asset, balance); + } + } + } + } +} \ No newline at end of file diff --git a/contracts/recovery/LTokenExtract.sol b/contracts/recovery/LTokenExtract.sol new file mode 100644 index 0000000..e1b1a30 --- /dev/null +++ b/contracts/recovery/LTokenExtract.sol @@ -0,0 +1,38 @@ +// ABOUTME: Recovery implementation for LToken contracts during protocol deprecation +// ABOUTME: Extracts available liquidity to multisig while maintaining upgradeability + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {LToken} from "../../src/tokens/LToken.sol"; +import {IERC20} from "../../src/interface/tokens/IERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {Errors} from "../../src/utils/Errors.sol"; + +contract LTokenExtract is LToken { + using SafeTransferLib for IERC20; + + event Recovered(address indexed asset, uint256 amount); + + bool public fundsRecovered; + address constant MULTISIG = 0x000000000000000000000000000000000000dEaD; + + function recoverFunds() external nonReentrant returns (uint256 amount) { + require(!fundsRecovered, "Already recovered"); + + updateState(); + + uint256 totalAssets = totalAssets(); + uint256 totalBorrows = borrows; + + require(totalAssets > totalBorrows, "No liquidity"); + + amount = totalAssets - totalBorrows; + fundsRecovered = true; + + IERC20(asset).safeTransfer(MULTISIG, amount); + + emit Recovered(address(asset), amount); + } + +} \ No newline at end of file diff --git a/scripts/extractFunds.js b/scripts/extractFunds.js new file mode 100644 index 0000000..598dcb7 --- /dev/null +++ b/scripts/extractFunds.js @@ -0,0 +1,137 @@ +// ABOUTME: Execute fund extraction from all Sentiment V1 contracts after upgrade +// ABOUTME: Handles LToken liquidity and account collateral extraction with batching + +const { ethers } = require('ethers'); + +const DEPLOYED_ADDRESSES = { + markets: { + USDT: '0x4c8e1656E042A206EEf7e8fcff99BaC667E4623e', + USDC: '0x0dDB1eA478F8eF0E22C7706D2903a41E94B1299B', + FRAX: '0x2E9963ae673A885b6bfeDa2f80132CE28b784C40', + ETH: '0xb190214D5EbAc7755899F2D96E519aa7a5776bEC', + WBTC: '0xe520C46d5Dab5bB80aF7Dc8b821f47deB4607DB2', + ARB: '0x21202227Bc15276E40d53889Bc83E59c3CccC121', + OHM: '0x37E6a0EcB9e8E5D90104590049a0A197E1363b67' + }, + core: { + Registry: '0x17B07cfBAB33C0024040e7C299f8048F4a49679B' + } +}; + +const MULTISIG = "0x000000000000000000000000000000000000dEaD"; // TODO: Update with actual multisig + +async function extractFunds(privateKey) { + const provider = new ethers.JsonRpcProvider(process.env.ARBITRUM_RPC_URL); + const signer = new ethers.Wallet(privateKey, provider); + + console.log('=== Starting Fund Extraction ==='); + console.log(`Recipient: ${MULTISIG}`); + console.log(`Signer: ${signer.address}`); + + // Phase 1: Extract LToken liquidity + console.log('\n--- Extracting LToken Liquidity ---'); + + for (const [name, address] of Object.entries(DEPLOYED_ADDRESSES.markets)) { + try { + const lToken = new ethers.Contract(address, [ + 'function recoverFunds() external returns (uint256)' + ], signer); + + const tx = await lToken.recoverFunds(); + console.log(`${name} recovery tx: ${tx.hash}`); + await tx.wait(); + } catch (error) { + console.error(`Failed to extract from ${name}:`, error.message); + } + } + + // Phase 2: Get all accounts with assets + console.log('\n--- Getting Accounts with Assets ---'); + + const registry = new ethers.Contract( + DEPLOYED_ADDRESSES.core.Registry, + ['function accounts(uint256) view returns (address)'], + provider + ); + + // Load accounts from our scan data + const fs = require('fs'); + const scanFiles = fs.readdirSync('./data').filter(f => f.includes('positions_final')); + if (scanFiles.length === 0) { + throw new Error('No position scan data found. Run smartPositionScan.js first.'); + } + + const latestScan = scanFiles.sort().pop(); + const scanData = JSON.parse(fs.readFileSync(`./data/${latestScan}`, 'utf8')); + const accountsWithAssets = scanData.positions.map(p => p.account); + + console.log(`Found ${accountsWithAssets.length} accounts with assets`); + + // Phase 3: Extract from all accounts + console.log('\n--- Extracting Account Collateral ---'); + + const BATCH_SIZE = 50; // Process in batches to avoid gas issues + let processed = 0; + let extracted = 0; + + for (let i = 0; i < accountsWithAssets.length; i += BATCH_SIZE) { + const batch = accountsWithAssets.slice(i, i + BATCH_SIZE); + + console.log(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(accountsWithAssets.length/BATCH_SIZE)}`); + + const promises = batch.map(async (accountAddress) => { + try { + const account = new ethers.Contract(accountAddress, [ + 'function recoverFunds() external' + ], signer); + + const tx = await account.recoverFunds({ + gasLimit: 200000 // Set reasonable gas limit + }); + await tx.wait(); + return { success: true, account: accountAddress, tx: tx.hash }; + } catch (error) { + return { success: false, account: accountAddress, error: error.message }; + } + }); + + const results = await Promise.allSettled(promises); + + results.forEach((result, idx) => { + processed++; + if (result.status === 'fulfilled' && result.value.success) { + extracted++; + console.log(`✓ ${result.value.account} - ${result.value.tx}`); + } else { + const error = result.status === 'rejected' ? result.reason : result.value.error; + console.log(`✗ ${batch[idx]} - ${error}`); + } + }); + + console.log(`Progress: ${processed}/${accountsWithAssets.length} processed, ${extracted} extracted`); + + // Brief pause between batches + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + console.log('\n=== Extraction Complete ==='); + console.log(`Total accounts processed: ${processed}`); + console.log(`Successfully extracted: ${extracted}`); + console.log(`Failed: ${processed - extracted}`); +} + +// CLI execution +if (require.main === module) { + const privateKey = process.argv[2]; + + if (!privateKey) { + console.log('Usage: node extractFunds.js '); + console.log('Example: node extractFunds.js 0xabc...'); + console.log(`Funds will be sent to hardcoded multisig: ${MULTISIG}`); + process.exit(1); + } + + extractFunds(privateKey).catch(console.error); +} + +module.exports = { extractFunds }; \ No newline at end of file From 094fa1d0bd0b6158de8cfea960fd66bb5da8b1ed Mon Sep 17 00:00:00 2001 From: sriyantra <85423375+sriyantra@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:36:04 -0700 Subject: [PATCH 2/2] chore: add multisig addr --- contracts/recovery/AccountExtract.sol | 2 +- contracts/recovery/LTokenExtract.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/recovery/AccountExtract.sol b/contracts/recovery/AccountExtract.sol index 613604d..81ed15d 100644 --- a/contracts/recovery/AccountExtract.sol +++ b/contracts/recovery/AccountExtract.sol @@ -11,7 +11,7 @@ import "../../src/interface/tokens/IERC20.sol"; contract AccountExtract is Account { /// @notice Hardcoded multisig address to receive extracted funds - address constant MULTISIG = 0x000000000000000000000000000000000000dEaD; // TODO: Update with actual multisig + address constant MULTISIG = 0x92f473Ef0Cd07080824F5e6B0859ac49b3AEb215; event Recovered(address indexed position, address indexed owner, address indexed asset, uint256 amount); diff --git a/contracts/recovery/LTokenExtract.sol b/contracts/recovery/LTokenExtract.sol index e1b1a30..0b54b66 100644 --- a/contracts/recovery/LTokenExtract.sol +++ b/contracts/recovery/LTokenExtract.sol @@ -15,7 +15,7 @@ contract LTokenExtract is LToken { event Recovered(address indexed asset, uint256 amount); bool public fundsRecovered; - address constant MULTISIG = 0x000000000000000000000000000000000000dEaD; + address constant MULTISIG = 0x92f473Ef0Cd07080824F5e6B0859ac49b3AEb215; function recoverFunds() external nonReentrant returns (uint256 amount) { require(!fundsRecovered, "Already recovered");