Skip to content

Commit 4b79e6f

Browse files
committed
feat: add recovery contracts
1 parent 5d84b60 commit 4b79e6f

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed

EXTRACTION_PLAN.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Sentiment V1 Protocol Extraction Plan
2+
3+
## Overview
4+
Extract all funds from deprecated Sentiment V1 protocol on Arbitrum to single recipient address.
5+
6+
## Control Structure
7+
- **ProxyAdmin**: `0x92f473Ef0Cd07080824F5e6B0859ac49b3AEb215`
8+
- **ProxyAdmin Owner**: `0x3e5c63644E683549055b9be8653de26E0B4cd36e`
9+
- **Controls**: 7 LTokens + Registry + AccountManager + Beacon (all user accounts)
10+
11+
## Extraction Flow
12+
13+
### Phase 1: Upgrade Contracts
14+
1. **Upgrade 7 LToken proxies** to `LTokenExtract` implementation
15+
2. **Upgrade Beacon** to `AccountExtract` implementation
16+
17+
### Phase 2: Extract LToken Liquidity (~$23k)
18+
Call `recoverFunds()` on each LToken:
19+
- USDT: $634
20+
- USDC: $1,241
21+
- FRAX: $7,416
22+
- WETH: $13,196
23+
- WBTC: $16
24+
- ARB: $567
25+
- OHM: $6
26+
27+
### Phase 3: Extract Account Collateral (~$65k)
28+
Call `recoverFunds()` on each of the 4,800+ accounts.
29+
30+
## Key Functions
31+
32+
**LTokenExtract.recoverFunds()**
33+
- Extracts available liquidity only (totalAssets - borrows)
34+
- Emits `Recovered(asset, amount)` event
35+
36+
**AccountExtract.recoverFunds()**
37+
- Loops through account's assets array
38+
- Transfers each token balance to multisig
39+
- Emits `Recovered(position, owner, asset, amount)` event per asset
40+
41+
## Execution Script
42+
Simple script that:
43+
1. Calls `recoverFunds()` on 7 LTokens
44+
2. Calls `recoverFunds()` on all accounts with assets
45+
3. Handles batching for gas limits
46+
4. Retries failed transactions
47+
48+
## User Claims
49+
After recovery, users query Dune for their `Recovered` events by owner address to see what funds they can claim from the multisig.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// ABOUTME: Account implementation that adds fund extraction capability
2+
// ABOUTME: Minimal addition to existing Account functionality for protocol deprecation
3+
4+
// SPDX-License-Identifier: MIT
5+
pragma solidity ^0.8.17;
6+
7+
import "../../src/core/Account.sol";
8+
import "../../src/interface/core/IRegistry.sol";
9+
import "../../src/interface/core/IAccountManager.sol";
10+
import "../../src/interface/tokens/IERC20.sol";
11+
12+
contract AccountExtract is Account {
13+
/// @notice Hardcoded multisig address to receive extracted funds
14+
address constant MULTISIG = 0x000000000000000000000000000000000000dEaD; // TODO: Update with actual multisig
15+
16+
event Recovered(address indexed position, address indexed owner, address indexed asset, uint256 amount);
17+
18+
function recoverFunds() external {
19+
// Get account owner from Registry
20+
address owner = IRegistry(IAccountManager(accountManager).registry()).ownerFor(address(this));
21+
22+
// Loop through all assets and transfer them
23+
for (uint i = 0; i < assets.length; i++) {
24+
address asset = assets[i];
25+
if (asset != address(0)) {
26+
IERC20 token = IERC20(asset);
27+
uint256 balance = token.balanceOf(address(this));
28+
if (balance > 0) {
29+
token.transfer(MULTISIG, balance);
30+
emit Recovered(address(this), owner, asset, balance);
31+
}
32+
}
33+
}
34+
}
35+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// ABOUTME: Recovery implementation for LToken contracts during protocol deprecation
2+
// ABOUTME: Extracts available liquidity to multisig while maintaining upgradeability
3+
4+
// SPDX-License-Identifier: MIT
5+
pragma solidity ^0.8.17;
6+
7+
import {LToken} from "../../src/tokens/LToken.sol";
8+
import {IERC20} from "../../src/interface/tokens/IERC20.sol";
9+
import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol";
10+
import {Errors} from "../../src/utils/Errors.sol";
11+
12+
contract LTokenExtract is LToken {
13+
using SafeTransferLib for IERC20;
14+
15+
event Recovered(address indexed asset, uint256 amount);
16+
17+
bool public fundsRecovered;
18+
address constant MULTISIG = 0x000000000000000000000000000000000000dEaD;
19+
20+
function recoverFunds() external nonReentrant returns (uint256 amount) {
21+
require(!fundsRecovered, "Already recovered");
22+
23+
updateState();
24+
25+
uint256 totalAssets = totalAssets();
26+
uint256 totalBorrows = borrows;
27+
28+
require(totalAssets > totalBorrows, "No liquidity");
29+
30+
amount = totalAssets - totalBorrows;
31+
fundsRecovered = true;
32+
33+
IERC20(asset).safeTransfer(MULTISIG, amount);
34+
35+
emit Recovered(address(asset), amount);
36+
}
37+
38+
}

scripts/extractFunds.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// ABOUTME: Execute fund extraction from all Sentiment V1 contracts after upgrade
2+
// ABOUTME: Handles LToken liquidity and account collateral extraction with batching
3+
4+
const { ethers } = require('ethers');
5+
6+
const DEPLOYED_ADDRESSES = {
7+
markets: {
8+
USDT: '0x4c8e1656E042A206EEf7e8fcff99BaC667E4623e',
9+
USDC: '0x0dDB1eA478F8eF0E22C7706D2903a41E94B1299B',
10+
FRAX: '0x2E9963ae673A885b6bfeDa2f80132CE28b784C40',
11+
ETH: '0xb190214D5EbAc7755899F2D96E519aa7a5776bEC',
12+
WBTC: '0xe520C46d5Dab5bB80aF7Dc8b821f47deB4607DB2',
13+
ARB: '0x21202227Bc15276E40d53889Bc83E59c3CccC121',
14+
OHM: '0x37E6a0EcB9e8E5D90104590049a0A197E1363b67'
15+
},
16+
core: {
17+
Registry: '0x17B07cfBAB33C0024040e7C299f8048F4a49679B'
18+
}
19+
};
20+
21+
const MULTISIG = "0x000000000000000000000000000000000000dEaD"; // TODO: Update with actual multisig
22+
23+
async function extractFunds(privateKey) {
24+
const provider = new ethers.JsonRpcProvider(process.env.ARBITRUM_RPC_URL);
25+
const signer = new ethers.Wallet(privateKey, provider);
26+
27+
console.log('=== Starting Fund Extraction ===');
28+
console.log(`Recipient: ${MULTISIG}`);
29+
console.log(`Signer: ${signer.address}`);
30+
31+
// Phase 1: Extract LToken liquidity
32+
console.log('\n--- Extracting LToken Liquidity ---');
33+
34+
for (const [name, address] of Object.entries(DEPLOYED_ADDRESSES.markets)) {
35+
try {
36+
const lToken = new ethers.Contract(address, [
37+
'function recoverFunds() external returns (uint256)'
38+
], signer);
39+
40+
const tx = await lToken.recoverFunds();
41+
console.log(`${name} recovery tx: ${tx.hash}`);
42+
await tx.wait();
43+
} catch (error) {
44+
console.error(`Failed to extract from ${name}:`, error.message);
45+
}
46+
}
47+
48+
// Phase 2: Get all accounts with assets
49+
console.log('\n--- Getting Accounts with Assets ---');
50+
51+
const registry = new ethers.Contract(
52+
DEPLOYED_ADDRESSES.core.Registry,
53+
['function accounts(uint256) view returns (address)'],
54+
provider
55+
);
56+
57+
// Load accounts from our scan data
58+
const fs = require('fs');
59+
const scanFiles = fs.readdirSync('./data').filter(f => f.includes('positions_final'));
60+
if (scanFiles.length === 0) {
61+
throw new Error('No position scan data found. Run smartPositionScan.js first.');
62+
}
63+
64+
const latestScan = scanFiles.sort().pop();
65+
const scanData = JSON.parse(fs.readFileSync(`./data/${latestScan}`, 'utf8'));
66+
const accountsWithAssets = scanData.positions.map(p => p.account);
67+
68+
console.log(`Found ${accountsWithAssets.length} accounts with assets`);
69+
70+
// Phase 3: Extract from all accounts
71+
console.log('\n--- Extracting Account Collateral ---');
72+
73+
const BATCH_SIZE = 50; // Process in batches to avoid gas issues
74+
let processed = 0;
75+
let extracted = 0;
76+
77+
for (let i = 0; i < accountsWithAssets.length; i += BATCH_SIZE) {
78+
const batch = accountsWithAssets.slice(i, i + BATCH_SIZE);
79+
80+
console.log(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(accountsWithAssets.length/BATCH_SIZE)}`);
81+
82+
const promises = batch.map(async (accountAddress) => {
83+
try {
84+
const account = new ethers.Contract(accountAddress, [
85+
'function recoverFunds() external'
86+
], signer);
87+
88+
const tx = await account.recoverFunds({
89+
gasLimit: 200000 // Set reasonable gas limit
90+
});
91+
await tx.wait();
92+
return { success: true, account: accountAddress, tx: tx.hash };
93+
} catch (error) {
94+
return { success: false, account: accountAddress, error: error.message };
95+
}
96+
});
97+
98+
const results = await Promise.allSettled(promises);
99+
100+
results.forEach((result, idx) => {
101+
processed++;
102+
if (result.status === 'fulfilled' && result.value.success) {
103+
extracted++;
104+
console.log(`✓ ${result.value.account} - ${result.value.tx}`);
105+
} else {
106+
const error = result.status === 'rejected' ? result.reason : result.value.error;
107+
console.log(`✗ ${batch[idx]} - ${error}`);
108+
}
109+
});
110+
111+
console.log(`Progress: ${processed}/${accountsWithAssets.length} processed, ${extracted} extracted`);
112+
113+
// Brief pause between batches
114+
await new Promise(resolve => setTimeout(resolve, 1000));
115+
}
116+
117+
console.log('\n=== Extraction Complete ===');
118+
console.log(`Total accounts processed: ${processed}`);
119+
console.log(`Successfully extracted: ${extracted}`);
120+
console.log(`Failed: ${processed - extracted}`);
121+
}
122+
123+
// CLI execution
124+
if (require.main === module) {
125+
const privateKey = process.argv[2];
126+
127+
if (!privateKey) {
128+
console.log('Usage: node extractFunds.js <private_key>');
129+
console.log('Example: node extractFunds.js 0xabc...');
130+
console.log(`Funds will be sent to hardcoded multisig: ${MULTISIG}`);
131+
process.exit(1);
132+
}
133+
134+
extractFunds(privateKey).catch(console.error);
135+
}
136+
137+
module.exports = { extractFunds };

0 commit comments

Comments
 (0)