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
62 changes: 32 additions & 30 deletions sui/its.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,35 +386,25 @@ async function migrateCoinMetadata(keypair, client, config, contracts, args, opt
}

// give_unlinked_coin
async function giveUnlinkedCoin(keypair, client, config, contracts, args, options) {
async function giveUnlinkedCoin(keypair, client, _, contracts, args, options) {
const { InterchainTokenService: itsConfig, AxelarGateway } = contracts;
const { InterchainTokenService } = itsConfig.objects;
const walletAddress = keypair.toSuiAddress();
const deployConfig = { client, keypair, options, walletAddress };
const [symbol, name, decimals] = args;
const [symbol, tokenId] = args;
const txBuilder = new TxBuilder(client);

if (options.salt) {
validateParameters({
isHexString: { salt: options.salt },
});
}
validateParameters({
isHexString: { tokenId },
});

// Deploy token on Sui
const [metadata, packageId, tokenType, treasuryCap] = await deployTokenFromInfo(deployConfig, symbol, name, decimals);
const coin = contracts[symbol.toUpperCase()];
if (!coin) throw new Error(`Cannot find coin with symbol ${symbol} in config`);

// Register deployed token (custom)
const [tokenId, _channelId, saltAddress, _result] = await registerCustomCoinUtil(
deployConfig,
itsConfig,
AxelarGateway,
symbol,
metadata,
tokenType,
null,
options.salt ? options.salt : null,
);
if (!tokenId) throw new Error(`error resolving token id from registration tx, got ${tokenId}`);
const decimals = coin.decimals;
const metadata = coin.objects.Metadata;
const packageId = coin.address;
const tokenType = coin.typeArgument;
const treasuryCap = coin.objects.TreasuryCap;

// TokenId
const tokenIdObject = await txBuilder.moveCall({
Expand All @@ -429,22 +419,29 @@ async function giveUnlinkedCoin(keypair, client, config, contracts, args, option
const treasuryCapOption = await txBuilder.moveCall({ target, arguments: callArguments, typeArguments });

// give_unlinked_coin<T>
const treasuryCapReclaimerOption = await txBuilder.moveCall({
const [treasuryCapReclaimerOption, channelOption] = await txBuilder.moveCall({
target: `${itsConfig.address}::interchain_token_service::give_unlinked_coin`,
arguments: [InterchainTokenService, tokenIdObject, metadata, treasuryCapOption],
typeArguments: [tokenType],
});

// TreasuryCapReclaimer<T>
const treasuryCapReclaimerType = [itsConfig.structs.TreasuryCapReclaimer, '<', tokenType, '>'].join('');
const channelType = AxelarGateway.structs.Channel;
if (options.treasuryCapReclaimer) {
const treasuryCapReclaimer = await txBuilder.moveCall({
target: `${STD_PACKAGE_ID}::option::extract`,
arguments: [treasuryCapReclaimerOption],
typeArguments: [treasuryCapReclaimerType],
});

txBuilder.tx.transferObjects([treasuryCapReclaimer], walletAddress);
const channel = await txBuilder.moveCall({
target: `${STD_PACKAGE_ID}::option::extract`,
arguments: [channelOption],
typeArguments: [channelType],
});

txBuilder.tx.transferObjects([treasuryCapReclaimer, channel], walletAddress);
}

await txBuilder.moveCall({
Expand All @@ -453,10 +450,16 @@ async function giveUnlinkedCoin(keypair, client, config, contracts, args, option
typeArguments: [treasuryCapReclaimerType],
});

await txBuilder.moveCall({
target: `${STD_PACKAGE_ID}::option::destroy_none`,
arguments: [channelOption],
typeArguments: [channelType],
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unconditional Destruction of Optional Values

The giveUnlinkedCoin function unconditionally calls destroy_none on treasuryCapReclaimerOption and channelOption. This fails because when options.treasuryCapReclaimer is true, their values are extracted, consuming them. When options.treasuryCapReclaimer is false, channelOption is never extracted, remaining a Some variant.

Fix in Cursor Fix in Web


const result = await broadcastFromTxBuilder(txBuilder, keypair, `Give Unlinked Coin (${symbol})`, options);

// Save the deployed token
saveTokenDeployment(packageId, tokenType, contracts, symbol, decimals, tokenId, treasuryCap, metadata, [], saltAddress);
saveTokenDeployment(packageId, tokenType, contracts, symbol, decimals, tokenId, treasuryCap, metadata, [], '');

// Save TreasuryCapReclaimer to coin config (if exists)
if (options.treasuryCapReclaimer && contracts[symbol.toUpperCase()]) {
Expand Down Expand Up @@ -1146,12 +1149,11 @@ if (require.main === module) {

const giveUnlinkedCoinProgram = new Command()
.name('give-unlinked-coin')
.command('give-unlinked-coin <symbol> <name> <decimals>')
.description(`Deploy a coin on Sui, register it as custom coin and give its treasury capability to ITS.`)
.command('give-unlinked-coin <symbol> <tokenId>')
.description(`Call give unlinked coin and give its treasury capability to ITS.`)
.addOption(new Option('--treasuryCapReclaimer', 'Pass this flag to retain the ability to reclaim the treasury capability'))
.addOption(new Option('--salt <salt>', 'An address in hexidecimal to be used as salt in the Token ID'))
.action((symbol, name, decimals, options) => {
mainProcessor(giveUnlinkedCoin, options, [symbol, name, decimals], processCommand);
.action((symbol, tokenId, options) => {
mainProcessor(giveUnlinkedCoin, options, [symbol, tokenId], processCommand);
});

const removeUnlinkedCoinProgram = new Command()
Expand Down
48 changes: 45 additions & 3 deletions sui/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,44 @@ async function processListCommand(keypair, client, args, options) {
await CoinManager.printCoins(client, coinTypeToCoins);
}

async function publishCoinCommand(keypair, client, args, options, contracts) {
const [symbol, name, decimals] = args;

validateParameters({
isNonEmptyString: {
symbol: symbol,
name: name,
decimals: decimals,
},
});

const walletAddress = keypair.toSuiAddress();

const config = loadConfig(options.env);
const chain = getChainConfig(config.chains, options.chainName);
await printWalletInfo(keypair, client, chain, options);

const deployConfig = { client, keypair, options, walletAddress };

// Deploy token on Sui
const [metadata, packageId, tokenType, treasuryCap] = await deployTokenFromInfo(deployConfig, symbol, name, decimals);

// Save the deployed token
saveTokenDeployment(packageId, tokenType, contracts, symbol, decimals, null, treasuryCap, metadata);
}

async function legacyCoinsCommand(keypair, client, args, options, contracts) {
const { InterchainTokenService: itsConfig } = contracts;
const { InterchainTokenService, InterchainTokenServicev0 } = itsConfig.objects;

if (options.createCoin) {
validateParameters({ isNonEmptyString: { symbol: options.createCoin } });
validateParameters({ isNonEmptyString: { decimals: options.decimals } });
validateParameters({ isNonEmptyString: { name: options.name } });
validateParameters({
isNonEmptyString: {
symbol: options.createCoin,
decimals: options.decimals,
name: options.name,
},
});

const config = loadConfig(options.env);
const chain = getChainConfig(config.chains, options.chainName);
Expand Down Expand Up @@ -313,6 +343,9 @@ if (require.main === module) {
const legacyCoinsProgram = new Command('legacy-coins').description(
'Save a list of legacy coins to be migrated to public coin metadata; and / or, create a legacy coin using the createCoin flag.',
);
const publishCoinProgram = new Command('publish-coin').description(
'Deploy a coin on Sui by specifying coin symbol, name and decimal precision',
);

// Define options, arguments, and actions for each sub-program
mergeProgram.option('--coin-type <coinType>', 'Coin type to merge').action((options) => {
Expand Down Expand Up @@ -340,11 +373,20 @@ if (require.main === module) {
mainProcessor(options, legacyCoinsCommand);
});

publishCoinProgram
.argument('<symbol>', 'Coin symbol')
.argument('<name>', 'Coin name')
.argument('<decimals>', 'Coin decimal precision')
.action((symbol, name, decimals, options) => {
mainProcessor(options, publishCoinCommand, [symbol, name, decimals]);
});

// Add sub-programs to the main program
program.addCommand(mergeProgram);
program.addCommand(splitProgram);
program.addCommand(listProgram);
program.addCommand(legacyCoinsProgram);
program.addCommand(publishCoinProgram);

// Add base options to all sub-programs
addOptionsToCommands(program, addBaseOptions);
Expand Down