UNPKG

@wormhole-foundation/sdk-evm-tokenbridge

Version:

SDK for EVM chains, used in conjunction with @wormhole-foundation/sdk

167 lines 8.95 kB
import { ErrNotWrapped, UniversalAddress, isNative, keccak256, nativeChainIds, serialize, toChain, toChainId, toNative, } from '@wormhole-foundation/sdk-connect'; import { ethers_contracts } from './index.js'; import { EvmAddress, EvmPlatform, EvmUnsignedTransaction, EvmZeroAddress, addChainId, addFrom, unusedArbiterFee, unusedNonce, WETH_CONTRACTS, } from '@wormhole-foundation/sdk-evm'; import '@wormhole-foundation/sdk-evm-core'; import { EvmWormholeCore } from '@wormhole-foundation/sdk-evm-core'; export class EvmTokenBridge { network; chain; provider; contracts; tokenBridge; core; tokenBridgeAddress; chainId; constructor(network, chain, provider, contracts) { this.network = network; this.chain = chain; this.provider = provider; this.contracts = contracts; this.chainId = nativeChainIds.networkChainToNativeChainId.get(network, chain); const tokenBridgeAddress = this.contracts.tokenBridge; if (!tokenBridgeAddress) throw new Error(`Wormhole Token Bridge contract for domain ${chain} not found`); this.tokenBridgeAddress = tokenBridgeAddress; this.tokenBridge = ethers_contracts.Bridge__factory.connect(this.tokenBridgeAddress, provider); this.core = new EvmWormholeCore(network, chain, provider, contracts); } static async fromRpc(provider, config) { const [network, chain] = await EvmPlatform.chainFromRpc(provider); const conf = config[chain]; if (conf.network !== network) throw new Error(`Network mismatch: ${conf.network} != ${network}`); return new EvmTokenBridge(network, chain, provider, conf.contracts); } async isWrappedAsset(token) { return await this.tokenBridge.isWrappedAsset(token.toString()); } async getOriginalAsset(token) { if (!(await this.isWrappedAsset(token))) throw ErrNotWrapped(token.toString()); const tokenContract = EvmPlatform.getTokenImplementation(this.provider, token.toString()); const [chain, address] = await Promise.all([ tokenContract.chainId().then(Number).then(toChainId).then(toChain), tokenContract.nativeContract().then((addr) => new UniversalAddress(addr)), ]); return { chain, address }; } async getTokenUniversalAddress(token) { return new EvmAddress(token).toUniversalAddress(); } async getTokenNativeAddress(originChain, token) { return new EvmAddress(token).toNative(); } async hasWrappedAsset(token) { try { await this.getWrappedAsset(token); return true; } catch (e) { } return false; } async getWrappedAsset(token) { if (isNative(token.address)) throw new Error('native asset cannot be a wrapped asset'); const wrappedAddress = await this.tokenBridge.wrappedAsset(toChainId(token.chain), token.address.toUniversalAddress().toString()); if (wrappedAddress === EvmZeroAddress) throw ErrNotWrapped(token.address.toUniversalAddress().toString()); return new EvmAddress(wrappedAddress); } async isTransferCompleted(vaa) { //The double keccak here is neccessary due to a fuckup in the original implementation of the // EVM core bridge: //Guardians don't sign messages (bodies) but explicitly hash them via keccak256 first. //However, they use an ECDSA scheme for signing where the first step is to hash the "message" // (which at this point is already the digest of the original message/body!) //Now, on EVM, ecrecover expects the final digest (i.e. a bytes32 rather than a dynamic bytes) // i.e. it does no hashing itself. Therefore the EVM core bridge has to hash the body twice // before calling ecrecover. But in the process of doing so, it erroneously sets the doubly // hashed value as vm.hash instead of using the only once hashed value. //And finally this double digest is then used in a mapping to store whether a VAA has already // been redeemed or not, which is ultimately the reason why we have to keccak the hash one // more time here. return this.tokenBridge.isTransferCompleted(keccak256(vaa.hash)); } async *createAttestation(token) { const messageFee = await this.core.getMessageFee(); const ignoredNonce = 0; yield this.createUnsignedTx(await this.tokenBridge.attestToken.populateTransaction(token.toString(), ignoredNonce, { value: messageFee, }), 'TokenBridge.createAttestation'); } async *submitAttestation(vaa) { const func = (await this.hasWrappedAsset({ ...vaa.payload.token, })) ? 'updateWrapped' : 'createWrapped'; yield this.createUnsignedTx(await this.tokenBridge[func].populateTransaction(serialize(vaa)), 'TokenBridge.' + func); } async *transfer(sender, recipient, token, amount, payload) { const senderAddr = new EvmAddress(sender).toString(); const recipientChainId = toChainId(recipient.chain); const recipientAddress = recipient.address .toUniversalAddress() .toUint8Array(); const messageFee = await this.core.getMessageFee(); if (isNative(token)) { const txReq = await (payload === undefined ? this.tokenBridge.wrapAndTransferETH.populateTransaction(recipientChainId, recipientAddress, unusedArbiterFee, unusedNonce, { value: amount + messageFee }) : this.tokenBridge.wrapAndTransferETHWithPayload.populateTransaction(recipientChainId, recipientAddress, unusedNonce, payload, { value: amount + messageFee })); yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.wrapAndTransferETH' + (payload === undefined ? '' : 'WithPayload')); } else { //TODO check for ERC-2612 (permit) support on token? const tokenAddr = new EvmAddress(token).toString(); const tokenContract = EvmPlatform.getTokenImplementation(this.provider, tokenAddr); const allowance = await tokenContract.allowance(senderAddr, this.tokenBridge.target); if (allowance < amount) { const txReq = await tokenContract.approve.populateTransaction(this.tokenBridge.target, amount); yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.Approve'); } const sharedParams = [ tokenAddr, amount, recipientChainId, recipientAddress, ]; const txReq = await (payload === undefined ? this.tokenBridge.transferTokens.populateTransaction(...sharedParams, unusedArbiterFee, unusedNonce, { value: messageFee }) : this.tokenBridge.transferTokensWithPayload.populateTransaction(...sharedParams, unusedNonce, payload, { value: messageFee })); yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.transferTokens' + (payload === undefined ? '' : 'WithPayload')); } } async *redeem(sender, vaa, unwrapNative = true) { const senderAddr = new EvmAddress(sender).toString(); if (vaa.payloadName === 'TransferWithPayload' && vaa.payload.token.chain !== this.chain) { const fromAddr = new EvmAddress(vaa.payload.from).unwrap(); if (fromAddr !== senderAddr) throw new Error(`VAA.from (${fromAddr}) does not match sender (${senderAddr})`); } if (vaa.payload.token.chain === this.chain) { const wrappedNativeAddr = await this.getWeth(); const tokenAddr = new EvmAddress(vaa.payload.token.address).unwrap(); if (tokenAddr === wrappedNativeAddr && unwrapNative) { const txReq = await this.tokenBridge.completeTransferAndUnwrapETH.populateTransaction(serialize(vaa)); yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.completeTransferAndUnwrapETH'); return; } } const txReq = await this.tokenBridge.completeTransfer.populateTransaction(serialize(vaa)); yield this.createUnsignedTx(addFrom(txReq, senderAddr), 'TokenBridge.completeTransfer'); } async getWrappedNative() { const address = await this.getWeth(); return toNative(this.chain, address); } async getWeth() { return WETH_CONTRACTS[this.network]?.[this.chain] ?? this.tokenBridge.WETH(); } createUnsignedTx(txReq, description, parallelizable = false) { return new EvmUnsignedTransaction(addChainId(txReq, this.chainId), this.network, this.chain, description, parallelizable); } } //# sourceMappingURL=tokenBridge.js.map