Skip to content

Commit

Permalink
Merge pull request #71 from DefiLlama/portal-sui
Browse files Browse the repository at this point in the history
Portal sui
  • Loading branch information
vrtnd authored Oct 23, 2023
2 parents 0bd92df + f6a5f18 commit 7e2e39d
Show file tree
Hide file tree
Showing 14 changed files with 668 additions and 16 deletions.
296 changes: 296 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"dependencies": {
"@defillama/sdk": "^4.0.43",
"@mysten/sui.js": "^0.44.0",
"async-retry": "^1.3.1",
"axios": "^0.21.0",
"axios-rate-limit": "^1.3.0",
Expand Down
6 changes: 6 additions & 0 deletions src/adapters/portal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EventData } from "../../utils/types";
import { getProvider } from "@defillama/sdk/build/general";
import { ethers } from "ethers";
import { PromisePool } from "@supercharge/promise-pool";
import { getSuiEvents } from "./sui";

// Wormhole: Portal core and token bridge contract addresses
// https://docs.wormhole.com/wormhole/blockchain-environments/environments
Expand Down Expand Up @@ -58,6 +59,10 @@ const contractAddresses = {
tokenBridge: "0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627",
coreBridge: "0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6",
},
sui: {
tokenBridge: "0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9", // object ID
coreBridge: "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c", // object ID
},
} as {
[chain: string]: {
tokenBridge: string;
Expand Down Expand Up @@ -336,6 +341,7 @@ const adapter: BridgeAdapter = {
optimism: constructParams("optimism"),
arbitrum: constructParams("arbitrum"),
base: constructParams("base"),
sui: getSuiEvents,
};

export default adapter;
167 changes: 167 additions & 0 deletions src/adapters/portal/sui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ethers } from "ethers";
import { getClient, getTransactionBlocks } from "../../helpers/sui";
import { EventData } from "../../utils/types";
import { SuiEvent, SuiObjectChange } from "@mysten/sui.js/dist/cjs/client";
import { normalizeSuiAddress, SUI_TYPE_ARG } from "@mysten/sui.js/utils";

const wormholeMessageEventType =
"0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a::publish_message::WormholeMessage";
const tokenBridgeAddress = "0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9";
const originalTokenBridgePackageId = "0x26efee2b51c911237888e5dc6702868abca3c7ac12c53f76ef8eba0697695e3d";

/**
* Retrieves Sui events from a given checkpoint range using the token bridge.
* Optimized to make as few RPC calls as possible.
* @param fromCheckpoint The starting checkpoint to retrieve events from.
* @param toCheckpoint The ending checkpoint to retrieve events from.
* @returns An array of EventData objects representing the events that occurred within the given checkpoint range.
*/
export const getSuiEvents = async (fromCheckpoint: number, toCheckpoint: number): Promise<EventData[]> => {
const events: EventData[] = [];
const txBlocks = await getTransactionBlocks(fromCheckpoint, toCheckpoint, tokenBridgeAddress);
for (const txBlock of txBlocks) {
if (
txBlock.effects?.status.status !== "success" ||
!txBlock.checkpoint ||
!txBlock.objectChanges ||
txBlock.transaction?.data.transaction.kind !== "ProgrammableTransaction"
) {
continue;
}
const transactions = txBlock.transaction.data.transaction.transactions;
for (const tx of transactions) {
const moveCall = "MoveCall" in tx && tx.MoveCall;
if (!moveCall || moveCall.package !== originalTokenBridgePackageId) {
continue;
}
if (
(moveCall.module === "complete_transfer_with_payload" && moveCall.function === "authorize_transfer") ||
(moveCall.module === "complete_transfer" && moveCall.function === "authorize_transfer")
) {
const token = moveCall.type_arguments![0];
// search backwards for the parse_and_verify call
const parseAndVerifyTx = transactions
.slice(
0,
transactions.findIndex((value) => value === tx)
)
.reverse()
.find(
(tx) => "MoveCall" in tx && tx.MoveCall.module === "vaa" && tx.MoveCall.function === "parse_and_verify"
);
if (!parseAndVerifyTx || !("MoveCall" in parseAndVerifyTx)) {
continue;
}
const vaaArg = parseAndVerifyTx.MoveCall.arguments?.[1];
if (!vaaArg || typeof vaaArg !== "object" || !("Input" in vaaArg)) {
continue;
}
const vaaInput = txBlock.transaction.data.transaction.inputs[vaaArg.Input];
if (!vaaInput || vaaInput.type !== "pure" || vaaInput.valueType !== "vector<u8>") {
continue;
}
const vaa = Buffer.from(vaaInput.value as number[]);
const sigStart = 6;
const numSigners = vaa[5];
const sigLength = 66;
const body = vaa.subarray(sigStart + sigLength * numSigners);
const payload = body.subarray(51);
const type = payload.readUInt8(0);
if (type !== 1 && type !== 3) {
continue;
}
const amount = await denormalizeAmount(token, ethers.BigNumber.from(payload.subarray(1, 33)));
const to = `0x${payload.subarray(67, 99).toString("hex")}`;
const event: EventData = {
blockNumber: Number(txBlock.checkpoint),
txHash: txBlock.digest,
// Wrapped tokens are minted from the zero address on Ethereum
// Override the from address to be the zero address for consistency
from: isWrappedToken(token, txBlock.objectChanges) ? ethers.constants.AddressZero : tokenBridgeAddress,
to,
token,
amount,
isDeposit: false,
};
events.push(event);
}
if (
((moveCall.module === "transfer_tokens_with_payload" && moveCall.function === "transfer_tokens_with_payload") ||
(moveCall.module === "transfer_tokens" && moveCall.function === "transfer_tokens")) &&
txBlock.events
) {
const token = tx.MoveCall.type_arguments![0];
const payload = getWormholeMessagePayload(txBlock.events);
const originChain = payload.readUint16BE(65);
const toChain = payload.readUInt16BE(99);
const amount = await denormalizeAmount(token, ethers.BigNumber.from(payload.subarray(1, 33)));
const isWrapped = isWrappedToken(token, txBlock.objectChanges);
const event: EventData = {
blockNumber: Number(txBlock.checkpoint),
txHash: txBlock.digest,
from: txBlock.transaction.data.sender,
// if this is a wrapped token being burned and not being sent to its origin chain,
// then it should be included in the volume by fixing the to address
to: !isWrapped || originChain !== toChain ? tokenBridgeAddress : ethers.constants.AddressZero,
token,
amount,
isDeposit: !isWrapped,
};
events.push(event);
}
}
}
return events;
};

const getWormholeMessagePayload = (events: SuiEvent[]): Buffer => {
const filtered = events.filter((event) => {
return event.type === wormholeMessageEventType;
});
// TODO: support multiple transfers in a single txBlock
if (filtered.length !== 1) {
throw new Error(`Expected exactly one wormhole message event, found ${filtered.length}`);
}
return Buffer.from((filtered[0].parsedJson as any).payload);
};

const tokenDecimalsCache: { [token: string]: number } = {};

const getTokenDecimals = async (token: string): Promise<number> => {
if (token in tokenDecimalsCache) {
return tokenDecimalsCache[token];
}
const client = getClient();
const coinMetadata = await client.getCoinMetadata({ coinType: token });
if (coinMetadata === null) {
throw new Error(`Failed to get coin metadata for ${token}`);
}
const { decimals } = coinMetadata;
tokenDecimalsCache[token] = decimals;
return decimals;
};

const denormalizeAmount = async (token: string, amount: ethers.BigNumber): Promise<ethers.BigNumber> => {
const decimals = await getTokenDecimals(token);
if (decimals > 8) {
return amount.mul(ethers.BigNumber.from(10).pow(decimals - 8));
}
return amount;
};

const isWrappedToken = (token: string, objectChanges: SuiObjectChange[]) => {
const split = token.split("::");
if (split.length !== 3) {
throw new Error(`Invalid token ${token}`);
}
const normalized = token === SUI_TYPE_ARG ? token : `${normalizeSuiAddress(split[0])}::${split[1]}::${split[2]}`;
const nativeKey = `0x2::dynamic_field::Field<${originalTokenBridgePackageId}::token_registry::Key<${normalized}>, ${originalTokenBridgePackageId}::native_asset::NativeAsset<${normalized}>>`;
const wrappedKey = `0x2::dynamic_field::Field<${originalTokenBridgePackageId}::token_registry::Key<${normalized}>, ${originalTokenBridgePackageId}::wrapped_asset::WrappedAsset<${normalized}>>`;
const value = objectChanges.find(
(change) => change.type === "mutated" && [nativeKey, wrappedKey].includes(change.objectType)
);
if (!value) {
throw new Error(`Failed to find object change for token ${normalized}`);
}
return value.type === "mutated" && value.objectType === wrappedKey;
};
93 changes: 93 additions & 0 deletions src/adapters/portal/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,98 @@ const testKlaytn = async () => {
//);
};

const testSui = async () => {
// const events = await adapter["sui"](14_992_284, 16_081_978)
// console.log(events.length)

// complete_transfer_with_payload (wrapped)
// https://suiexplorer.com/txblock/GWgFCab4BqtxXV2mFvMdM5deAkpKUPSqapT1AreoBh4Y
let checkpoint = 15736900;
let event = await getEvent(checkpoint, "sui");
assertEqual(
{
blockNumber: checkpoint,
txHash: "GWgFCab4BqtxXV2mFvMdM5deAkpKUPSqapT1AreoBh4Y",
from: ethers.constants.AddressZero,
to: "0xc4c610707eab9b222996b075f7d07c7d9b07766ab7bcafef621fd53bbf089f4e",
token: "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN",
amount: ethers.BigNumber.from("34389000000"),
isDeposit: false,
},
event
);

// complete_transfer (native)
// https://suiexplorer.com/txblock/2XrjjNwGXPzEDdHztJ7kG6E9wijiWK8sfQczhoT1V38Q
checkpoint = 16007453;
event = await getEvent(checkpoint, "sui");
assertEqual(
{
blockNumber: checkpoint,
txHash: "2XrjjNwGXPzEDdHztJ7kG6E9wijiWK8sfQczhoT1V38Q",
from: "0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9",
to: "0xd5c67d73166147f6fec91717187651966cc15c5caec2462dbbe380f44b21e87f",
token: "0x2::sui::SUI",
amount: ethers.BigNumber.from("10000000"),
isDeposit: false,
},
event
);

// transfer_tokens_with_payload (wrapped - not sent to origin chain)
// https://suiexplorer.com/txblock/EqSqsc9pbo6hRgAhUyjn3nsKU51k6kEHd1v4DVBdvkyz
checkpoint = 15827121;
event = await getEvent(checkpoint, "sui");
assertEqual(
{
blockNumber: checkpoint,
txHash: "EqSqsc9pbo6hRgAhUyjn3nsKU51k6kEHd1v4DVBdvkyz",
from: "0x161a9493ce468ee0fe56be02fe086eb47b650f76cbc8f7030a8f9b2bbcc7f3ac",
to: "0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9",
token: "0xaf8cd5edc19c4512f4259f0bee101a40d41ebed738ade5874359610ef8eeced5::coin::COIN",
amount: ethers.BigNumber.from("1000000"),
isDeposit: false,
},
event
);

// transfer_tokens (wrapped - sent to origin chain)
// https://suiexplorer.com/txblock/2Dc96jf7PSJeA9kcLxUyTZMsSwsEtTDMrYdabqDpuZAS
checkpoint = 16005422;
event = await getEvent(checkpoint, "sui");
assertEqual(
{
blockNumber: checkpoint,
txHash: "2Dc96jf7PSJeA9kcLxUyTZMsSwsEtTDMrYdabqDpuZAS",
from: "0xd5c67d73166147f6fec91717187651966cc15c5caec2462dbbe380f44b21e87f",
to: ethers.constants.AddressZero,
token: "0xb7844e289a8410e50fb3ca48d69eb9cf29e27d223ef90353fe1bd8e27ff8f3f8::coin::COIN",
amount: ethers.BigNumber.from("1000000"),
isDeposit: false,
},
event
);

// transfer_tokens (native)
// https://suiexplorer.com/txblock/9ePHxgVdKFoYGnE4nMg3bxiShmYq9yuYKdENEtwDKVwm
checkpoint = 15991909;
event = await getEvent(checkpoint, "sui");
assertEqual(
{
blockNumber: checkpoint,
txHash: "9ePHxgVdKFoYGnE4nMg3bxiShmYq9yuYKdENEtwDKVwm",
from: "0xbda9efe864e492f5921f30287a10f60287eafdcc82f259a39bb2335fb069a948",
to: "0xc57508ee0d4595e5a8728974a4a93a787d38f339757230d441e895422c07aba9",
token: "0x2::sui::SUI",
amount: ethers.BigNumber.from("2100000000"),
isDeposit: true,
},
event
);

console.log("sui tests passed");
};

(async () => {
await Promise.all([
testNoEventsFound(),
Expand All @@ -457,5 +549,6 @@ const testKlaytn = async () => {
testAvalanche(),
testOptimism(),
testKlaytn(),
testSui(),
]);
})();
2 changes: 1 addition & 1 deletion src/adapters/test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getLatestBlock } from "@defillama/sdk/build/util";
import { getLatestBlock } from "../utils/blocks";
import { Chain } from "@defillama/sdk/build/general";
import adapters from "./";
import { importBridgeNetwork } from "../data/importBridgeNetwork";
Expand Down
1 change: 1 addition & 0 deletions src/data/bridgeNetworkData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export default [
"Optimism",
"Arbitrum",
"Base",
"Sui"
],
chainMapping: {
avalanche: "avax", // this is needed temporarily, need to fix and remove
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/checkDbEntries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { wrapScheduledLambda } from "../utils/wrap";
import { getLatestBlock } from "@defillama/sdk/build/util";
import { getLatestBlockNumber } from "../utils/blocks";
import {
queryAggregatedHourlyTimestampRange,
queryAggregatedDailyTimestampRange,
Expand Down Expand Up @@ -73,7 +73,7 @@ export default wrapScheduledLambda(async (_event) => {
Object.keys(recordedBlocks).map(async (adapter: any) => {
const chain = adapter.split(":")[1];
if (!latestChainBlocks[chain]) {
latestChainBlocks[chain] = (await getLatestBlock(chain)).number;
latestChainBlocks[chain] = await getLatestBlockNumber(chain);
}
})
);
Expand Down
52 changes: 52 additions & 0 deletions src/helpers/sui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
SuiClient,
SuiTransactionBlockResponse,
getFullnodeUrl,
PaginatedTransactionResponse,
} from "@mysten/sui.js/client";

export const getClient = () => {
const url = process.env.SUI_RPC ?? getFullnodeUrl("mainnet");
return new SuiClient({ url });
};

export const getTransactionBlocks = async (
fromCheckpoint: number,
toCheckpoint: number,
changedObject: string
): Promise<SuiTransactionBlockResponse[]> => {
const client = getClient();
const results: SuiTransactionBlockResponse[] = [];
let hasNextPage = false;
let cursor: string | null | undefined = undefined;
let oldestCheckpoint: string | null = null;
do {
// TODO: The public RPC doesn't support fetching events by chaining filters with a `TimeRange` filter,
// so we have to search backwards for our checkpoint range
const response: PaginatedTransactionResponse = await client.queryTransactionBlocks({
filter: { ChangedObject: changedObject },
cursor,
options: {
showEffects: true,
showEvents: true,
showInput: true,
showObjectChanges: true,
},
});
for (const txBlock of response.data) {
const checkpoint = txBlock.checkpoint;
if (!checkpoint) {
continue;
}
if (checkpoint >= fromCheckpoint.toString() && checkpoint <= toCheckpoint.toString()) {
results.push(txBlock);
}
if (oldestCheckpoint === null || checkpoint < oldestCheckpoint) {
oldestCheckpoint = checkpoint;
}
}
hasNextPage = response.hasNextPage;
cursor = response.nextCursor;
} while (hasNextPage && cursor && oldestCheckpoint && oldestCheckpoint >= fromCheckpoint.toString());
return results;
};
Loading

0 comments on commit 7e2e39d

Please sign in to comment.