Skip to content

feat: Solana rebalancing #2261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 37 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cec5aab
chore: bump sdk
bmzig May 9, 2025
0fe58f3
update tests
bmzig May 12, 2025
14fdd63
Merge branch 'master' into bz/sdk
bmzig May 12, 2025
b074a46
Merge branch 'master' into bz/sdk
bmzig May 12, 2025
e9f6e17
Merge branch 'master' into bz/sdk
bmzig May 12, 2025
2e38dd0
feat: Solana rebalancing
bmzig May 12, 2025
3f87f12
Merge branch 'master' into bz/sdk
bmzig May 13, 2025
876beda
Merge branch 'master' into bz/sdk
bmzig May 13, 2025
c81b78d
Merge branch 'bz/sdk' into bz/solRebalance
bmzig May 13, 2025
ec65a9d
update with new bridge type
bmzig May 13, 2025
ae8615f
Merge branch 'master' into bz/solRebalance
bmzig May 16, 2025
efc26ea
Merge branch 'master' into bz/solRebalance
bmzig May 19, 2025
69953bf
svm
bmzig May 19, 2025
bc3acf4
block finder
bmzig May 19, 2025
2af255f
wip
bmzig May 19, 2025
90b147e
improve: Lax assumption of EVMSpokePoolClients
bmzig May 20, 2025
d15ad93
tests
bmzig May 20, 2025
2bbaebb
Merge branch 'master' into bz/sdkAgain
bmzig May 20, 2025
deaa3d9
Merge branch 'bz/sdkAgain' into bz/solRebalance
bmzig May 21, 2025
0da2148
Merge branch 'master' into bz/sdkAgain
bmzig May 21, 2025
5a76de8
Merge branch 'bz/sdkAgain' into bz/solRebalance
bmzig May 21, 2025
89b7ea5
Merge branch 'master' into bz/sdkAgain
bmzig May 21, 2025
039df8d
update
bmzig May 21, 2025
010cc30
Merge branch 'bz/sdkAgain' into bz/solRebalance
bmzig May 21, 2025
e3bc755
scaffold
bmzig May 21, 2025
8654ffa
l2 -> l1 finalizer and rebalancer
bmzig May 22, 2025
adba8d1
Merge remote-tracking branch 'origin/master' into bz/sdkAgain
pxrl May 23, 2025
39fb7ae
squash
pxrl May 23, 2025
dc44057
Merge branch 'master' into bz/sdkAgain
bmzig May 23, 2025
0238290
comment
bmzig May 23, 2025
157ad37
Merge branch 'bz/sdkAgain' into bz/solRebalance
bmzig May 23, 2025
e477b5d
Merge branch 'master' into bz/solRebalance
bmzig May 23, 2025
24eada3
Merge branch 'master' into bz/solRebalance
bmzig May 23, 2025
d19c0f2
wip
bmzig May 23, 2025
5bade2d
Merge branch 'master' into bz/solRebalance
bmzig May 26, 2025
e8b5ff6
Merge branch 'master' into bz/solRebalance
bmzig May 27, 2025
a0bc7e0
wip
bmzig May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@across-protocol/sdk": "4.2.5",
"@arbitrum/sdk": "^4.0.2",
"@consensys/linea-sdk": "^0.2.1",
"@coral-xyz/borsh": "^0.30.1",
"@coral-xyz/anchor": "^0.31.1",
"@defi-wonderland/smock": "^2.3.5",
"@eth-optimism/sdk": "^3.3.2",
"@ethersproject/abi": "^5.7.0",
Expand All @@ -27,11 +27,11 @@
"@maticnetwork/maticjs": "^3.6.0",
"@maticnetwork/maticjs-ethers": "^1.0.3",
"@openzeppelin/hardhat-upgrades": "^1.28.0",
"@solana/web3.js": "2.0.0",
"@uma/common": "2.33.0",
"@uma/logger": "^1.3.0",
"axios": "^1.7.4",
"binance-api-node": "0.12.7",
"bs58": "^6.0.0",
"dotenv": "^16.3.1",
"ethers": "^5.7.2",
"hardhat": "^2.14.0",
Expand All @@ -42,6 +42,7 @@
"redis4": "npm:redis@^4.1.0",
"superstruct": "^1.0.3",
"ts-node": "^10.9.1",
"tweetnacl": "1.0.0",
"viem": "^2.26.1",
"winston": "^3.10.0",
"zksync-ethers": "^5.7.2"
Expand Down
162 changes: 162 additions & 0 deletions src/adapter/bridges/SolanaUsdcCCTPBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Contract, Signer } from "ethers";
import { BridgeTransactionDetails, BaseBridgeAdapter, BridgeEvents } from "./BaseBridgeAdapter";
import {
BigNumber,
EventSearchConfig,
TOKEN_SYMBOLS_MAP,
compareAddressesSimple,
assert,
toBN,
getCctpDomainForChainId,
Address,
EvmAddress,
SvmAddress,
paginatedEventQuery,
ZERO_BYTES,
SVMProvider,
isDefined,
getSvmSignerFromEvmSigner,
Wallet,
ethers,
} from "../../utils";
import { processEvent } from "../utils";
import { getCctpTokenMessenger, isCctpV2L2ChainId } from "../../utils/CCTPUtils";
import { CCTP_NO_DOMAIN } from "@across-protocol/constants";
import { arch } from "@across-protocol/sdk";
import { TokenMessengerMinterIdl } from "@across-protocol/contracts";
import bs58 from "bs58";

type MintAndWithdrawData = {
mintRecipient: string;
amount: bigint;
};

export class SolanaUsdcCCTPBridge extends BaseBridgeAdapter {
private CCTP_MAX_SEND_AMOUNT = toBN(1_000_000_000_000); // 1MM USDC.
private IS_CCTP_V2 = false;
private readonly l1UsdcTokenAddress: EvmAddress;
private readonly solanaMessageTransmitter: SvmAddress;
// We need the constructor to operate in a synchronous context, but the call to construct an event client is asynchronous, so
// this bridge holds onto the client promise and lazily evaluates it for when it needs to use it (in `queryL2BridgeFinalizationEvents`).
private readonly solanaEventsClientPromise: Promise<arch.svm.SvmCpiEventsClient>;
private solanaEventsClient: arch.svm.SvmCpiEventsClient;
private svmAddress: string;

constructor(l2chainId: number, hubChainId: number, l1Signer: Signer, l2Provider: SVMProvider) {
super(l2chainId, hubChainId, l1Signer, [EvmAddress.from(getCctpTokenMessenger(l2chainId, hubChainId).address)]);
assert(
getCctpDomainForChainId(l2chainId) !== CCTP_NO_DOMAIN && getCctpDomainForChainId(hubChainId) !== CCTP_NO_DOMAIN,
"Unknown CCTP domain ID"
);
this.IS_CCTP_V2 = isCctpV2L2ChainId(l2chainId);

const { address: l1Address, abi: l1Abi } = getCctpTokenMessenger(l2chainId, hubChainId);
this.l1Bridge = new Contract(l1Address, l1Abi, l1Signer);

const { address: l2Address } = getCctpTokenMessenger(l2chainId, l2chainId);
this.solanaMessageTransmitter = SvmAddress.from(l2Address);
this.solanaEventsClientPromise = arch.svm.SvmCpiEventsClient.createFor(
l2Provider,
l2Address,
TokenMessengerMinterIdl
);
this.svmAddress = this._getEquivalentSvmAddress();

this.l1UsdcTokenAddress = EvmAddress.from(TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]);
}

private get l2DestinationDomain(): number {
return getCctpDomainForChainId(this.l2chainId);
}

protected resolveL2TokenAddress(l1Token: EvmAddress): string {
l1Token;
return TOKEN_SYMBOLS_MAP.USDC.addresses[this.l2chainId];
}

async constructL1ToL2Txn(
toAddress: Address,
l1Token: EvmAddress,
_l2Token: Address,
amount: BigNumber
): Promise<BridgeTransactionDetails> {
assert(compareAddressesSimple(l1Token.toAddress(), TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]));
const signer = await this.l1Signer.getAddress();
assert(compareAddressesSimple(signer, toAddress.toEvmAddress()), "Cannot rebalance to a non-signer address");
amount = amount.gt(this.CCTP_MAX_SEND_AMOUNT) ? this.CCTP_MAX_SEND_AMOUNT : amount;
return Promise.resolve({
contract: this.getL1Bridge(),
method: "depositForBurn",
args: this.IS_CCTP_V2
? [
amount,
this.l2DestinationDomain,
this._getEquivalentSvmAddress(),
this.l1UsdcTokenAddress.toAddress(),
ZERO_BYTES, // Anyone can finalize the message on domain when this is set to bytes32(0)
0, // maxFee set to 0 so this will be a "standard" speed transfer
2000, // Hardcoded minFinalityThreshold value for standard transfer
]
: [amount, this.l2DestinationDomain, this._getEquivalentSvmAddress(), this.l1UsdcTokenAddress.toAddress()],
});
}

async queryL1BridgeInitiationEvents(
l1Token: EvmAddress,
fromAddress: EvmAddress,
toAddress: Address,
eventConfig: EventSearchConfig
): Promise<BridgeEvents> {
assert(compareAddressesSimple(l1Token.toAddress(), TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]));
const eventFilterArgs = this.IS_CCTP_V2
? [this.l1UsdcTokenAddress.toAddress(), undefined, fromAddress.toAddress()]
: [undefined, this.l1UsdcTokenAddress.toAddress(), undefined, fromAddress.toAddress()];
const eventFilter = this.getL1Bridge().filters.DepositForBurn(...eventFilterArgs);
const events = (await paginatedEventQuery(this.getL1Bridge(), eventFilter, eventConfig)).filter((event) =>
compareAddressesSimple(event.args.mintRecipient, toAddress.toBytes32())
);
return {
[this.resolveL2TokenAddress(l1Token)]: events.map((event) => processEvent(event, "amount")),
};
}

async queryL2BridgeFinalizationEvents(
l1Token: EvmAddress,
fromAddress: EvmAddress,
toAddress: Address,
eventConfig: EventSearchConfig
): Promise<BridgeEvents> {
// Lazily evaluate the events client.
this.solanaEventsClient ??= await this.solanaEventsClientPromise;
assert(compareAddressesSimple(l1Token.toAddress(), TOKEN_SYMBOLS_MAP.USDC.addresses[this.hubChainId]));
const l2FinalizationEvents = await this.solanaEventsClient.queryDerivedAddressEvents(
"MintAndWithdraw",
this.solanaMessageTransmitter.toV2Address(),
BigInt(eventConfig.from),
BigInt(eventConfig.to)
);
return {
[this.resolveL2TokenAddress(l1Token)]: l2FinalizationEvents
.map((event) => {
const data = event.data as MintAndWithdrawData;
if (String(data.mintRecipient) !== toAddress.toBase58()) {
return undefined;
}
return {
amount: toBN(data.amount),
blockNumber: Number(event.slot),
txnRef: event.signature,
// There is no log/transaction index on Solana.
txnIndex: 0,
logIndex: 0,
};
})
.filter(isDefined),
};
}

_getEquivalentSvmAddress(): string {
const svmSigner = getSvmSignerFromEvmSigner(this.l1Signer as Wallet);
return ethers.utils.hexlify(bs58.decode(svmSigner.publicKey.toBase58()));
}
}
1 change: 1 addition & 0 deletions src/adapter/bridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export * from "./UsdcCCTPBridge";
export * from "./ZKStackBridge";
export * from "./ZKStackUSDCBridge";
export * from "./ZKStackWethBridge";
export * from "./SolanaUsdcCCTPBridge";
export * from "./OFTBridge";
66 changes: 37 additions & 29 deletions src/clients/TokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { CachingMechanismInterface, L1Token, Deposit } from "../interfaces";
import {
BigNumber,
bnZero,
chainIsEvm,
Contract,
dedupArray,
ERC20,
Expand All @@ -24,6 +23,7 @@ import {
getTokenInfo,
isEVMSpokePoolClient,
assert,
isSVMSpokePoolClient,
} from "../utils";

export type TokenDataType = { [chainId: number]: { [token: string]: { balance: BigNumber; allowance: BigNumber } } };
Expand Down Expand Up @@ -223,41 +223,49 @@ export class TokenClient {
chainId: number,
hubPoolTokens: L1Token[]
): Promise<Record<string, { balance: BigNumber; allowance: BigNumber }>> {
if (!chainIsEvm(chainId)) {
return {}; // @todo
}

const spokePoolClient = this.spokePoolClients[chainId];

assert(isEVMSpokePoolClient(spokePoolClient));
const multicall3 = sdkUtils.getMulticall3(chainId, spokePoolClient.spokePool.provider);
if (!isDefined(multicall3)) {
return this.fetchTokenData(chainId, hubPoolTokens);
}
if (isEVMSpokePoolClient(spokePoolClient)) {
const multicall3 = sdkUtils.getMulticall3(chainId, spokePoolClient.spokePool.provider);
if (!isDefined(multicall3)) {
return this.fetchTokenData(chainId, hubPoolTokens);
}

const { relayerAddress } = this;
const balances: sdkUtils.Call3[] = [];
const allowances: sdkUtils.Call3[] = [];
this.resolveRemoteTokens(chainId, hubPoolTokens).forEach((token) => {
balances.push({ contract: token, method: "balanceOf", args: [relayerAddress] });
allowances.push({
contract: token,
method: "allowance",
args: [relayerAddress, spokePoolClient.spokePoolAddress.toEvmAddress()],
const { relayerAddress } = this;
const balances: sdkUtils.Call3[] = [];
const allowances: sdkUtils.Call3[] = [];
this.resolveRemoteTokens(chainId, hubPoolTokens).forEach((token) => {
balances.push({ contract: token, method: "balanceOf", args: [relayerAddress] });
allowances.push({
contract: token,
method: "allowance",
args: [relayerAddress, spokePoolClient.spokePoolAddress.toEvmAddress()],
});
});
});

const calls = [...balances, ...allowances];
const results = await sdkUtils.aggregate(multicall3, calls);
const calls = [...balances, ...allowances];
const results = await sdkUtils.aggregate(multicall3, calls);

const allowanceOffset = balances.length;
const balanceInfo = Object.fromEntries(
balances.map(({ contract: { address } }, idx) => {
return [address, { balance: results[idx][0], allowance: results[allowanceOffset + idx][0] }];
})
);
const allowanceOffset = balances.length;
const balanceInfo = Object.fromEntries(
balances.map(({ contract: { address } }, idx) => {
return [address, { balance: results[idx][0], allowance: results[allowanceOffset + idx][0] }];
})
);

return balanceInfo;
return balanceInfo;
} else if (isSVMSpokePoolClient(spokePoolClient)) {
return Object.fromEntries(
hubPoolTokens
.map((token) => {
const remoteToken = getRemoteTokenForL1Token(token.address, chainId, {
chainId: this.hubPoolClient.chainId,
});
return isDefined(remoteToken) ? [remoteToken, { balance: toBN(0), allowance: toBN(0) }] : undefined;
})
.filter(isDefined)
);
}
}

async update(): Promise<void> {
Expand Down
31 changes: 18 additions & 13 deletions src/clients/bridges/AdapterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getRemoteTokenForL1Token,
getTokenInfo,
isEVMSpokePoolClient,
isSVMSpokePoolClient,
} from "../../utils";
import { SpokePoolClient, HubPoolClient } from "../";
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants";
Expand Down Expand Up @@ -71,18 +72,24 @@ export class AdapterManager {
return {};
} // Special case for the EthereumAdapter

if (!chainIsEvm(chainId)) {
return; // @todo
}

return Object.fromEntries(
SUPPORTED_TOKENS[chainId]?.map((symbol) => {
const spokePoolClient = spokePoolClients[chainId];
assert(isEVMSpokePoolClient(spokePoolClient));
const l2Signer = spokePoolClient.spokePool.signer;
let l2SignerOrProvider;
if (isEVMSpokePoolClient(spokePoolClient)) {
l2SignerOrProvider = spokePoolClient.spokePool.signer;
} else if (isSVMSpokePoolClient(spokePoolClient)) {
l2SignerOrProvider = spokePoolClient.svmEventsClient.getRpc();
}
const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[hubChainId];
const bridgeConstructor = CUSTOM_BRIDGE[chainId]?.[l1Token] ?? CANONICAL_BRIDGE[chainId];
const bridge = new bridgeConstructor(chainId, hubChainId, l1Signer, l2Signer, EvmAddress.from(l1Token));
const bridge = new bridgeConstructor(
chainId,
hubChainId,
l1Signer,
l2SignerOrProvider,
EvmAddress.from(l1Token)
);
return [l1Token, bridge];
}) ?? []
);
Expand All @@ -92,8 +99,10 @@ export class AdapterManager {
return {};
}
const spokePoolClient = spokePoolClients[chainId];
assert(isEVMSpokePoolClient(spokePoolClient));
const l2Signer = spokePoolClient.spokePool.signer;
let l2Signer;
if (isEVMSpokePoolClient(spokePoolClient)) {
l2Signer = spokePoolClient.spokePool.signer;
}
return Object.fromEntries(
SUPPORTED_TOKENS[chainId]
?.map((symbol) => {
Expand All @@ -109,10 +118,6 @@ export class AdapterManager {
);
};
Object.values(this.spokePoolClients).map(({ chainId }) => {
if (!chainIsEvm(chainId)) {
return; // @todo
}

// Instantiate a generic adapter and supply all network-specific configurations.
this.adapters[chainId] = new BaseChainAdapter(
spokePoolClients,
Expand Down
28 changes: 19 additions & 9 deletions src/common/ClientHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,26 @@ export async function constructSpokePoolClientsWithStartBlocks(
const spokePoolSigners = await getSpokePoolSigners(baseSigner, enabledChains);
const spokePools = await Promise.all(
enabledChains.map(async (chainId) => {
const spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]);
// TODO: initialize using typechain factory after V3.5 migration.
// const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]);
const spokePoolContract = new ethers.Contract(
spokePoolAddr,
[...SpokePool.abi, ...V3_SPOKE_POOL_ABI],
spokePoolSigners[chainId]
const registrationBlock = Number(
process.env[`REGISTRATION_BLOCK_OVERRIDE_${chainId}`] ??
(await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]))
);
const registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]);
return { chainId, contract: spokePoolContract, registrationBlock };
// const registrationBlock = await resolveSpokePoolActivationBlock(chainId, hubPoolClient, toBlockOverride[1]);
if (chainIsEvm(chainId)) {
const spokePoolAddr = hubPoolClient.getSpokePoolForBlock(chainId, toBlockOverride[1]);
// TODO: initialize using typechain factory after V3.5 migration.
// const spokePoolContract = SpokePool.connect(spokePoolAddr, spokePoolSigners[chainId]);
const spokePoolContract = new ethers.Contract(
spokePoolAddr,
[...SpokePool.abi, ...V3_SPOKE_POOL_ABI],
spokePoolSigners[chainId]
);
return { chainId, contract: spokePoolContract, registrationBlock };
} else {
// The hub pool client can only return the truncated address of the SVM spoke pool, so if the chain is non-evm, then fallback
// to the definitions in the contracts repository.
return { chainId, contract: undefined, registrationBlock };
}
})
);

Expand Down
Loading
Loading