Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
175 changes: 175 additions & 0 deletions scripts/receiveStray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* Recovers the 1513.878129304 SUI mistakenly transferred to the stSUI CollectionFeeCap.
*
* The coin is owned by AddressOwner(<cap id>), so it can only be pulled out by
* `liquid_staking::receive_stray` — added in AlphaFiTech/alpha#bugfix/lst-receive-stray. Until
* that upgrade is live the script reports and stops; it checks the function exists before
* building anything.
*
* npx tsx scripts/receiveStray.ts # dry run
* npx tsx scripts/receiveStray.ts --execute # sign and submit
* RECIPIENT=0x… npx tsx scripts/receiveStray.ts # send somewhere other than the signer
*
* The cap is address-owned, so PK_B64 must be the cap owner's key regardless of RECIPIENT.
*/
import { Transaction } from "@mysten/sui/transactions";
import { isValidSuiAddress, normalizeSuiAddress } from "@mysten/sui/utils";
import { getConf } from "../src/index.ts";
import {
dryRunTransactionBlock,
executeTransactionBlock,
getExecStuff,
getGrpcClient,
} from "./utils.ts";

const MODULE = "liquid_staking";
const FUNCTION = "receive_stray";

const CAP = "0x019466989adf3cf8320f8e7ab45a44c6f8ce9688125852b60c82475d2d2f9849";
const STRAY_COIN =
"0xb2bedbc05a022505fb1c8564e39c9f8f19e176f20563197f8abe2c9cc9fc99aa";
const STRAY_TYPE = "0x2::coin::Coin<0x2::sui::SUI>";

/**
* Builds a PTB that receives `stray` out of `collectionFeeCap` and transfers it to `address`.
*
* `receive_stray` returns the object rather than transferring it, so the destination is decided
* here instead of being baked into the package.
*/
export function buildReceiveStrayTx(
packageId: string,
collectionFeeCap: string,
lstCoinType: string,
stray: { objectId: string; version: string; digest: string; type: string },
address: string,
): Transaction {
const txb = new Transaction();

const [received] = txb.moveCall({
target: `${packageId}::${MODULE}::${FUNCTION}`,
typeArguments: [lstCoinType, stray.type],
arguments: [
txb.object(collectionFeeCap),
txb.receivingRef({
objectId: stray.objectId,
version: stray.version,
digest: stray.digest,
}),
],
});

txb.transferObjects([received], address);
return txb;
}

/**
* Sui's BCS address serializer runs normalizeSuiAddress(), which LEFT-PADS rather than
* rejecting — a truncated paste like "0x019466989a" becomes the valid, unowned address
* 0x00…019466989a, and transferring there is irreversible. isValidSuiAddress requires the full
* normalized form, so it rejects exactly those. Callers print the normalized value, never the
* raw input.
*/
function requireAddress(label: string, value: string): string {
if (!isValidSuiAddress(value)) {
console.error(
`${label} is not a valid Sui address: ${JSON.stringify(value)}\n` +
`Expected the full 66-character 0x-prefixed form. Short values are silently ` +
`zero-padded into an address nobody owns.`,
);
process.exit(1);
}
return normalizeSuiAddress(value);
}

async function main() {
const execute = process.argv.includes("--execute");
const packageId = process.env.PACKAGE_ID ?? getConf().STSUI_LATEST_PACKAGE_ID;
const { address: signer } = getExecStuff();
const recipient = process.env.RECIPIENT
? requireAddress("RECIPIENT", process.env.RECIPIENT)
: signer;
const client = getGrpcClient();

// Fetched, never hardcoded: receivingRef needs the object's current version and digest.
const { objects } = await client.core.getObjects({
objectIds: [CAP, STRAY_COIN],
include: { json: true },
});
const [cap, coin] = objects;
if (cap instanceof Error) throw new Error(`cap: ${cap.message}`);
if (coin instanceof Error) throw new Error(`stray coin: ${coin.message}`);

const owner =
cap.owner.$kind === "AddressOwner" ? cap.owner.AddressOwner : undefined;
const coinOwner =
coin.owner.$kind === "AddressOwner" ? coin.owner.AddressOwner : undefined;
if (coinOwner !== CAP) {
console.log(`already recovered — ${STRAY_COIN} is now owned by ${coinOwner}`);
return;
}

const lstCoinType = cap.type.slice(
cap.type.indexOf("<") + 1,
cap.type.lastIndexOf(">"),
);

console.log(`package : ${packageId}`);
console.log(`cap : ${CAP} (owner ${owner})`);
console.log(`stray : ${STRAY_COIN} v${coin.version}`);
console.log(`amount : ${(coin.json as { balance: string }).balance} MIST`);
console.log(`type arg : ${lstCoinType}`);
console.log(`signer : ${signer}`);
console.log(`recipient: ${recipient}`);

try {
await client.core.getMoveFunction({
packageId,
moduleName: MODULE,
name: FUNCTION,
});
} catch {
console.log(
`\nNOT RECOVERABLE YET — ${packageId} has no ${MODULE}::${FUNCTION}. ` +
`Land the package upgrade, then bump STSUI_LATEST_PACKAGE_ID in src/common/ids.ts.`,
);
return;
}

if (owner !== signer) {
console.log(
`\nWRONG SIGNER — the cap is owned by ${owner}, PK_B64 resolves to ${signer}.`,
);
return;
}

const txb = buildReceiveStrayTx(
packageId,
CAP,
lstCoinType,
{
objectId: STRAY_COIN,
version: coin.version,
digest: coin.digest,
type: STRAY_TYPE,
},
recipient,
);

let ok: boolean;
if (execute) {
console.log("\nexecuting…");
ok = await executeTransactionBlock(txb);
} else {
console.log("\ndry run (pass --execute to submit):");
ok = await dryRunTransactionBlock(txb, signer);
}

if (!ok) {
process.exit(1);
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
57 changes: 41 additions & 16 deletions scripts/test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { exec } from "child_process";
import { stSuiExchangeRate } from "../src/common/utils.ts";
import {
getConf,
create_lst,
set_validators,
setValidators as setValidatorsTx,
mint as mintStsui,
redeem,
collect_fee,
refresh,
updateFees,
getFees,
FeeConfig,
updateFee,
Events,
fetchStSuiAPR,
fetchStSuiAPY,
Expand All @@ -32,7 +29,7 @@ async function createLst() {
1,
600,
10000,
{ address },
address,
);
// dryRunTransactionBlock(txb);
executeTransactionBlock(txb);
Expand All @@ -41,7 +38,10 @@ async function createLst() {
//createLst();

async function setValidators() {
const txb = await set_validators(
const txb = await setValidatorsTx(
getConf().LST_INFO,
getConf().ADMIN_CAP,
getConf().STSUI_COIN_TYPE,
["0xcb7efe4253a0fe58df608d8a2d3c0eea94b4b40a8738c8daae4eb77830c16cd7"],
[100],
);
Expand All @@ -53,7 +53,12 @@ async function setValidators() {

async function mint() {
const { address } = getExecStuff();
const txb = await mintStsui("1000000000", { address });
const txb = await mintStsui(
getConf().LST_INFO,
getConf().STSUI_COIN_TYPE,
"1000000000",
address,
);
if (txb) {
// dryRunTransactionBlock(txb);
executeTransactionBlock(txb);
Expand All @@ -63,7 +68,12 @@ async function mint() {

async function redeemstsui() {
const { address } = getExecStuff();
const txb = await redeem("100899000", { address });
const txb = await redeem(
getConf().LST_INFO,
getConf().STSUI_COIN_TYPE,
"100899000",
address,
);
if (txb) {
// dryRunTransactionBlock(txb);
executeTransactionBlock(txb);
Expand All @@ -73,21 +83,26 @@ async function redeemstsui() {

async function collectFee() {
const { address } = getExecStuff();
const txb = await collect_fee({ address });
const txb = await collect_fee(
getConf().LST_INFO,
getConf().STSUI_COIN_TYPE,
getConf().COLLECTION_FEE_CAP_ID,
address,
);
if (txb) {
executeTransactionBlock(txb);
}
}
// collectFee();

async function xrate() {
console.log(await stSuiExchangeRate());
// console.log((await getFees()) as FeeConfig);
console.log(await stSuiExchangeRate(getConf().LST_INFO, true));
// console.log(await getFees(getConf().LST_INFO, true));
}
// xrate();

async function refreshh() {
const txb = await refresh();
const txb = await refresh(getConf().LST_INFO, getConf().STSUI_COIN_TYPE);
if (txb) {
executeTransactionBlock(txb);
// dryRunTransactionBlock(txb);
Expand All @@ -96,7 +111,15 @@ async function refreshh() {
// refreshh();

async function update_fee() {
const txb = await updateFees(0, 2, 1000, 10000);
const txb = await updateFee(
getConf().LST_INFO,
getConf().ADMIN_CAP,
getConf().STSUI_COIN_TYPE,
0,
2,
1000,
10000,
);
if (txb) {
dryRunTransactionBlock(txb);
// executeTransactionBlock(txb);
Expand All @@ -108,6 +131,7 @@ async function epochEvents() {
let ok = await Events.getEpochChangeEvents({
startTime: 1733776365964,
endTime: 1733827977386,
typeName: getConf().STSUI_COIN_TYPE,
});
console.log(ok);
}
Expand All @@ -116,7 +140,7 @@ async function epochEvents() {
async function apr() {
console.log(await fetchStSuiAPR(1));
}
apr();
// apr();
async function apy() {
console.log(await fetchStSuiAPY(1));
}
Expand All @@ -134,13 +158,14 @@ async function totalStakers() {
async function fetchTotalStakerss() {
console.log(await fetchTotalStakers());
}
// fetchTotalStakerss()
fetchTotalStakerss();

async function getRedeemEvents() {
console.log(
await Events.getRedeemEvents({
startTime: 1735660741000,
endTime: 1735660801000,
typeName: getConf().STSUI_COIN_TYPE,
}),
);
}
Expand Down
Loading
Loading