wormhole/algorand/audit_test/src/wormhole/Wormhole.ts

263 lines
9.8 KiB
TypeScript

import { decodeAddress, getApplicationAddress, LogicSigAccount, OnApplicationComplete, Transaction } from 'algosdk'
import { Address, AppId } from '../sdk/AlgorandTypes'
import { concatArrays, encodeUint16 } from "../sdk/Encoding"
import { Deployer, SignCallback, IStateInfo } from "../sdk/Deployer"
import path from 'path'
import assert from 'assert'
import { SignedVAA, WormholeSigner } from './WormholeTypes'
import { signWormholeMessage } from './WormholeEncoders'
import { generateInitVAA } from './WormholeVAA'
import { EMITTER_GUARDIAN, WormholeTmplSig } from './WormholeTmplSig'
export class Wormhole {
public static BASE_PATH = path.resolve(__dirname, '../../../')
public static CORE_STATE_MAP: IStateInfo = {
local: {
'meta': 'bytes',
'\x00': 'bytes',
'\x01': 'bytes',
'\x02': 'bytes',
'\x03': 'bytes',
'\x04': 'bytes',
'\x05': 'bytes',
'\x06': 'bytes',
'\x07': 'bytes',
'\x08': 'bytes',
'\x09': 'bytes',
'\x0A': 'bytes',
'\x0B': 'bytes',
'\x0C': 'bytes',
'\x0D': 'bytes',
'\x0E': 'bytes',
},
global: {
'MessageFee': 'uint',
'currentGuardianSetIndex': 'uint',
'booted': 'bytes',
'vphash': 'bytes',
'validUpdateApproveHash': 'bytes',
'validUpdateClearHash': 'bytes',
},
}
public static BRIDGE_STATE_MAP: IStateInfo = {
local: {},
global: {
'coreid': 'uint',
'coreAddr': 'bytes',
'validUpdateApproveHash': 'bytes',
'validUpdateClearHash': 'bytes',
'chain\x00': 'bytes',
'chain\x01': 'bytes',
'chain\x02': 'bytes',
'chain\x03': 'bytes',
'chain\x04': 'bytes',
'chain\x05': 'bytes',
'chain\x06': 'bytes',
'chain\x07': 'bytes',
'chain\x08': 'bytes',
'chain\x09': 'bytes',
'chain\x0A': 'bytes',
'chain\x0B': 'bytes',
'chain\x0C': 'bytes',
'chain\x0D': 'bytes',
'chain\x0E': 'bytes',
'chain\x0F': 'bytes',
'chain\x10': 'bytes',
'chain\x11': 'bytes',
'chain\x12': 'bytes',
'chain\x13': 'bytes',
'chain\x14': 'bytes',
'chain\x15': 'bytes',
'chain\x16': 'bytes',
'chain\x17': 'bytes',
'chain\x18': 'bytes',
'chain\x19': 'bytes',
'chain\x1A': 'bytes',
},
}
public constructor(
private readonly deployer: Deployer,
private readonly owner: Address,
public readonly coreId: AppId,
public readonly bridgeId: AppId,
private readonly signCallback: SignCallback,
) {}
public static async deployAndFund(
deployer: Deployer,
owner: Address,
signCallback: SignCallback,
signers: WormholeSigner[],
): Promise<Wormhole> {
// Generate paths
const corePath = path.join(Wormhole.BASE_PATH, 'wormhole_core.py')
const bridgePath = path.join(Wormhole.BASE_PATH, 'token_bridge.py')
// Deploy core contract
const coreApp = await deployer.makeSourceApp(corePath, Wormhole.CORE_STATE_MAP)
const coreCompiled = await deployer.makeApp(coreApp)
const coreDeployId = await deployer.deployApplication(owner, coreCompiled, signCallback)
const coreId = (await deployer.waitForTransactionResponse(coreDeployId))["application-index"]
console.log(`Wormhole core ID: ${coreId}`)
console.log(`Core address: ${getApplicationAddress(coreId)}, base64: ${Buffer.from(decodeAddress(getApplicationAddress(coreId)).publicKey).toString('base64')}`)
// Deploy token bridge
const bridgeApp = await deployer.makeSourceApp(bridgePath, Wormhole.BRIDGE_STATE_MAP)
const bridgeCompiled = await deployer.makeApp(bridgeApp)
// FIXME: The application address should be generated contract side for security
const bridgeArgs = [coreId, decodeAddress(getApplicationAddress(coreId)).publicKey]
const bridgeDeployId = await deployer.deployApplication(owner, bridgeCompiled, signCallback, undefined, bridgeArgs)
const bridgeId = (await deployer.waitForTransactionResponse(bridgeDeployId))["application-index"]
console.log(`Wormhole bridge ID: ${bridgeId}`)
console.log(`Bridge address: ${getApplicationAddress(bridgeId)}, base64: ${Buffer.from(decodeAddress(getApplicationAddress(bridgeId)).publicKey).toString('base64')}`)
// Create object
const result = new Wormhole(deployer, owner, coreId, bridgeId, signCallback)
// Initialize applications
const initUnsigned = generateInitVAA(signers, coreId)
const initialVaa = await signWormholeMessage(initUnsigned)
const initTxId = await result.sendSignedVAA(initialVaa)
await deployer.waitForTransactionResponse(initTxId)
console.log(`Wormhole initialization complete`)
return result
}
private static splitKeysAndSignatures(vaa: SignedVAA): {sigData: Uint8Array[], keyData: Uint8Array[]} {
const subSetSize = 7
const sigData = []
const keyData = []
for (let start = 0; start < vaa.signatures.length; start += subSetSize) {
const end = start + subSetSize
sigData.push(concatArrays(vaa.signatures.slice(start, end)))
keyData.push(concatArrays(vaa.keys.slice(start, end)))
}
return {sigData, keyData}
}
public async compileVaaVerify(deployer: Deployer): Promise<LogicSigAccount> {
const vaaVerifyPath = path.join(Wormhole.BASE_PATH, 'vaa_verify.py')
const vaaVerifyApp = await deployer.compileStateless(vaaVerifyPath)
return vaaVerifyApp
}
public async sendSignedVAA(vaa: SignedVAA, dryrunDebug = false): Promise<string> {
// Generate deduplication template sig
const deduplicationEmitter = concatArrays([encodeUint16(vaa.chainId), vaa.emitter])
const bits_per_sig = 8 * 15 * 127
const sequence = Math.floor(vaa.sequence / bits_per_sig)
const dedupTmplSig = new WormholeTmplSig(sequence, deduplicationEmitter, this.coreId)
console.log(`TmplSig address: ${dedupTmplSig.address}, base64: ${Buffer.from(decodeAddress(dedupTmplSig.address).publicKey).toString('base64')}`)
// Generate guardian template sig
const guardianTmplSig = new WormholeTmplSig(vaa.gsIndex, EMITTER_GUARDIAN, this.coreId)
// Opt-in tmplsig
const templateSigs = [dedupTmplSig, guardianTmplSig].concat(vaa.extraTmplSigs)
const logicSigMap: Map<Address, LogicSigAccount> = new Map()
for (const sig of templateSigs) {
await sig.optin(this.deployer, this.owner, this.coreId, this.signCallback, dryrunDebug)
logicSigMap.set(sig.address, sig.logicSig)
}
// Generate VAA verifier
const vaaVerify = await this.compileVaaVerify(this.deployer)
const vaaVerifyId = decodeAddress(vaaVerify.lsig.address()).publicKey
console.log(`VAA Verify address: ${vaaVerify.address()}, base64: ${Buffer.from(decodeAddress(vaaVerify.address()).publicKey).toString('base64')}`)
// Generate init transaction group
const accounts = [dedupTmplSig.address, guardianTmplSig.address].concat(vaa.extraTmplSigs.map((tmplSig) => tmplSig.address))
let txns: Transaction[]
switch (vaa.command) {
case 'init': {
txns = await Promise.all([
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['nop', Math.floor(Math.random() * 2 ** 32)]),
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['nop', Math.floor(Math.random() * 2 ** 32)]),
this.deployer.makeCallTransaction(
this.owner,
this.coreId,
OnApplicationComplete.NoOpOC,
['init', vaa.data, vaaVerifyId],
accounts,
),
this.deployer.makePayTransaction(this.owner, vaaVerify.address(), BigInt(100_000)),
])
break
}
case 'governance': {
const {sigData, keyData} = Wormhole.splitKeysAndSignatures(vaa)
assert(sigData.length === 3)
assert(keyData.length === 3)
// Assertions mirroring underlying contract code
assert(accounts.length >= 2)
assert(sigData.length > 0)
sigData.map((sigs, i) => {
let offset = 6
const sigLength = sigs.length
const vaaData = vaa.data.slice(offset, offset + sigLength)
if (Buffer.compare(vaaData, sigs) !== 0) {
console.log(`On entry ${i}, expected VAA sig data: ${Buffer.from(vaaData).toString('hex')}\nGot: ${Buffer.from(sigs).toString('hex')}`)
} else {
console.log(`Data matched expected for entry ${i}`)
}
let guardianCounter = 0
const endPointer = offset + sigLength
const keyBuffers: Uint8Array[] = []
while (offset < endPointer) {
const guardian = sigs[offset]
if (guardian !== guardianCounter) {
// TODO: This is broken and does not generate the same output as the contract!
console.log(`Expected ${guardianCounter}, got ${guardian}`)
}
const guardianKey = keyData[i].slice(guardian * 20, guardian * 20 + 20)
keyBuffers.push(guardianKey)
offset += 66
guardianCounter++
}
const allKeyData = concatArrays(keyBuffers)
if (Buffer.compare(allKeyData, keyData[i]) !== 0) {
console.log(`On entry ${i}, expected key data ${Buffer.from(keyData[i]).toString('hex')}, got ${Buffer.from(allKeyData).toString('hex')}`)
} else {
console.log(`Key matched expected for entry ${i}`)
}
})
// Generate transactions
txns = await Promise.all([
this.deployer.makeCallTransaction(vaaVerify.address(), this.coreId, OnApplicationComplete.NoOpOC, ['verifySigs', sigData[0], keyData[0], vaa.hash], accounts, [], [], '', 0),
this.deployer.makeCallTransaction(vaaVerify.address(), this.coreId, OnApplicationComplete.NoOpOC, ['verifySigs', sigData[1], keyData[1], vaa.hash], accounts, [], [], '', 0),
this.deployer.makeCallTransaction(vaaVerify.address(), this.coreId, OnApplicationComplete.NoOpOC, ['verifySigs', sigData[2], keyData[2], vaa.hash], accounts, [], [], '', 0),
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['verifyVAA', vaa.data], accounts, [], [], '', 0),
this.deployer.makeCallTransaction(this.owner, this.coreId, OnApplicationComplete.NoOpOC, ['governance', vaa.data], accounts, [], [], '', 5000),
])
logicSigMap.set(vaaVerify.address(), vaaVerify)
break
}
default: {
throw new Error(`Unknown command ${vaa.command}`)
}
}
// Call init group
const initTxId = await this.deployer.callGroupTransaction(txns, logicSigMap, this.signCallback, dryrunDebug)
return initTxId
}
}