From 3f6cea196151e215d664ec1f17d3ba6765435cc4 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Jul 2023 14:45:17 +0200 Subject: [PATCH 1/3] Prototyping canonical SolanaTBTC token with the Solang compiler Solang compiles Solidity code that can be run on Solana blockchain. SolanaTBTC contract uses the spl_token lib which is ERC20 equivalent on Solana. There were a couple of functionalities added to match L2TBTC.sol contract that was deployed to Arbitrum, Optimims and Polygon. However, the contracts that are on L2s do not match 1:1 with this one. E.g. there is no 'recoverERC20' function. Upgrade and pause (freeze) functionality should work out of the box on Solana, hence there are no functions written for it. setup.ts was largely copied from Solang official repository. --- cross-chain/solana/.gitignore | 9 + cross-chain/solana/SolanaTBTC.sol | 119 ++++++ cross-chain/solana/SolanaTBTC.spec.ts | 378 ++++++++++++++++++ cross-chain/solana/package.json | 40 ++ cross-chain/solana/setup.ts | 206 ++++++++++ .../solana/solana-library/spl_token.sol | 286 +++++++++++++ .../solana-library/system_instruction.sol | 300 ++++++++++++++ cross-chain/solana/tsconfig.json | 63 +++ 8 files changed, 1401 insertions(+) create mode 100644 cross-chain/solana/.gitignore create mode 100644 cross-chain/solana/SolanaTBTC.sol create mode 100644 cross-chain/solana/SolanaTBTC.spec.ts create mode 100644 cross-chain/solana/package.json create mode 100644 cross-chain/solana/setup.ts create mode 100644 cross-chain/solana/solana-library/spl_token.sol create mode 100644 cross-chain/solana/solana-library/system_instruction.sol create mode 100644 cross-chain/solana/tsconfig.json diff --git a/cross-chain/solana/.gitignore b/cross-chain/solana/.gitignore new file mode 100644 index 000000000..a5637d55c --- /dev/null +++ b/cross-chain/solana/.gitignore @@ -0,0 +1,9 @@ +*.js +*.so +*.key +*.json +!tsconfig.json +!package.json +node_modules +yarn.lock +/test-ledger diff --git a/cross-chain/solana/SolanaTBTC.sol b/cross-chain/solana/SolanaTBTC.sol new file mode 100644 index 000000000..16c8e4aeb --- /dev/null +++ b/cross-chain/solana/SolanaTBTC.sol @@ -0,0 +1,119 @@ +import './solana-library/spl_token.sol'; +import 'solana'; + +contract SolanaTBTC { + /// @notice Indicates if the given address is a minter. Only minters can + /// mint the token. + mapping(address => bool) isMinter; + + /// @notice List of all minters. + address[] minters; + + /// @notice owner + address authority; + + /// @notice mint account + /// @dev Stores information about the token itself. E.g. current supply and + /// its authorities + address mint; + + event MinterAdded(address minter); + event MinterRemoved(address minter); + + modifier needs_authority() { + for (uint64 i = 0; i < tx.accounts.length; i++) { + AccountInfo ai = tx.accounts[i]; + if (ai.key == authority && ai.is_signer) { + _; + return; + } + } + + print("Not signed by authority"); + revert("Not signed by authority"); + } + + modifier minter_only() { + for (uint64 i = 0; i < tx.accounts.length; i++) { + AccountInfo ai = tx.accounts[i]; + print("checking if a minter"); + if (isMinter[ai.key] && ai.is_signer) { + _; + return; + } + } + + print("Not a minter"); + revert("Not a minter"); + } + + constructor(address initial_authority) { + authority = initial_authority; + } + + /// @notice Adds the address to the minters list. + /// @dev Requirements: + /// - The caller must have authority. + /// - `minter` must not be a minter address already. + /// @param minter The address to be added as a minter. This address can mint + /// SolanaTBTC token. + function add_minter(address minter) needs_authority public { + print("adding a minter..."); + require(!isMinter[minter], "This address is already a minter"); + isMinter[minter] = true; + minters.push(minter); + emit MinterAdded(minter); + print("added a minter..."); + } + + /// @notice Removes the address from the minters list. + /// @dev Requirements: + /// - The caller must have authority. + /// - `minter` must be a minter address. + /// @param minter The address to be removed from the minters list. + function remove_minter(address minter) public needs_authority { + require(isMinter[minter], "This address is not a minter"); + delete isMinter[minter]; + + // We do not expect too many minters so a simple loop is safe. + for (uint256 i = 0; i < minters.length; i++) { + if (minters[i] == minter) { + minters[i] = minters[minters.length - 1]; + minters.pop(); + break; + } + } + + emit MinterRemoved(minter); + } + + function set_mint(address _mint) needs_authority public { + mint = _mint; + } + + function mint_to(address account, address _authority, uint64 amount) minter_only public { + print("yo, I'm in mint_to"); + SplToken.mint_to(mint, account, _authority, amount); + } + + function total_supply() public view returns (uint64) { + return SplToken.total_supply(mint); + } + + function get_balance(address account) public view returns (uint64) { + return SplToken.get_balance(account); + } + + function transfer(address from, address to, address owner, uint64 amount) public { + SplToken.transfer(from, to, owner, amount); + } + + function burn(address account, address owner, uint64 amount) public { + SplToken.burn(account, mint, owner, amount); + } + + /// @notice Allows to fetch a list of all minters. + function get_minters() public view returns (address[] memory) { + return minters; + } +} \ No newline at end of file diff --git a/cross-chain/solana/SolanaTBTC.spec.ts b/cross-chain/solana/SolanaTBTC.spec.ts new file mode 100644 index 000000000..94f910542 --- /dev/null +++ b/cross-chain/solana/SolanaTBTC.spec.ts @@ -0,0 +1,378 @@ +import { + getOrCreateAssociatedTokenAccount, + createMint, + Account, +} from "@solana/spl-token" +import { Keypair, PublicKey, Connection } from "@solana/web3.js" +import { Program, Provider, BN } from "@project-serum/anchor" +import expect from "expect" +import { loadContract, loadKey } from "./setup" + +describe("SolanaTBTC token", function () { + this.timeout(500000) + + let program: Program + let storage: Keypair + let payer: Keypair + let thirdParty: Keypair + let provider: Provider + let authority: Keypair + let guardianAuthority: Keypair + let connection: Connection + + before(async () => { + payer = loadKey("payer.key") // SolanaTBTC authority (deployer) + thirdParty = loadKey("thirdParty.key") // third party account + authority = Keypair.generate() // mint authority + guardianAuthority = Keypair.generate() // guardians can freeze minting + ;({ provider, storage, program } = await loadContract("SolanaTBTC", [ + payer.publicKey, + ])) + + connection = provider.connection + }) + + describe("adding and removing minters", () => { + it("should check for empty minters array", async () => { + const minters = await program.methods + .getMinters() + .accounts({ dataAccount: storage.publicKey }) + .view() + + expect(minters.length).toEqual(0) + }) + + it("should add a minter", async () => { + await program.methods + .addMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + const minters = await program.methods + .getMinters() + .accounts({ dataAccount: storage.publicKey }) + .view() + + expect(minters.length).toEqual(1) + expect(minters[0]).toEqual(authority.publicKey) + }) + + it("should remove a minter", async () => { + await program.methods + .removeMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + const minters = await program.methods + .getMinters() + .accounts({ dataAccount: storage.publicKey }) + .view() + + expect(minters.length).toEqual(0) + }) + }) + + describe("when a caller has no `mint setting` authority", () => { + it("should revert", async () => { + const mint = await createMint( + connection, + payer, + authority.publicKey, // mint authority + guardianAuthority.publicKey, // freeze authority + 18 // 18 decimals like TBTC on Ethereum + ) + + // third party is not the owner (authority) + try { + await program.methods + .setMint(mint) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + } catch (e: any) { + expect(e.logs).toContain("Program log: Not signed by authority") + } + }) + }) + + describe("when a caller has no 'adding a minter' authority", () => { + it("should revert", async () => { + // third party has no authority + try { + await program.methods + .addMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + } catch (e: any) { + expect(e.logs).toContain("Program log: Not signed by authority") + } + }) + }) + + describe("when a caller is not a minter", () => { + it("should revert", async () => { + const mint = await createMint( + connection, + payer, + authority.publicKey, // mint authority + guardianAuthority.publicKey, // freeze authority + 18 // 18 decimals like TBTC on Ethereum + ) + + const tokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + payer.publicKey + ) + + await program.methods + .addMinter(payer.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + try { + await program.methods + .mintTo(tokenAccount.address, thirdParty.publicKey, new BN(100000)) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: tokenAccount.address, isSigner: false, isWritable: true }, + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + } catch (e: any) { + expect(e.logs).toContain("Program log: Not a minter") + } + }) + }) + + describe("when callers have authority", () => { + let mint: PublicKey + let tokenAccount: Account + let otherTokenAccount: Account + let balance: BN + let totalSupply: BN + + it("should create a mint account and a token account with 0 balance", async () => { + mint = await createMint( + connection, + payer, + authority.publicKey, // mint authority + guardianAuthority.publicKey, // aka freeze authority + 18 // 18 decimals like TBTC on Ethereum + ) + + // payer is the owner (authority) + await program.methods + .setMint(mint) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(0) + + tokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + payer.publicKey + ) + + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(0) + }) + + it("should mint tokens", async () => { + await program.methods + .addMinter(authority.publicKey) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + await program.methods + .mintTo(tokenAccount.address, authority.publicKey, new BN(100000)) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: tokenAccount.address, isSigner: false, isWritable: true }, + { pubkey: authority.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([authority]) + .rpc() + + // check the balances + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(100000) + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(100000) + }) + + it("should transfer tokens", async () => { + otherTokenAccount = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + thirdParty.publicKey + ) + + await program.methods + .transfer( + tokenAccount.address, + otherTokenAccount.address, + payer.publicKey, + new BN(70000) + ) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: true, + }, + { pubkey: tokenAccount.address, isSigner: false, isWritable: true }, + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([payer]) + .rpc() + + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(100000) + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(30000) + + balance = await program.methods + .getBalance(otherTokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: false, + }, + ]) + .view() + + expect(balance.toNumber()).toBe(70000) + }) + + it("should burn tokens", async () => { + await program.methods + .burn(otherTokenAccount.address, thirdParty.publicKey, new BN(20000)) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: true, + }, + { pubkey: mint, isSigner: false, isWritable: true }, + { pubkey: thirdParty.publicKey, isSigner: true, isWritable: true }, + ]) + .signers([thirdParty]) + .rpc() + + totalSupply = await program.methods + .totalSupply() + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: mint, isSigner: false, isWritable: false }, + ]) + .view() + + expect(totalSupply.toNumber()).toBe(80000) + balance = await program.methods + .getBalance(tokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { pubkey: tokenAccount.address, isSigner: false, isWritable: false }, + ]) + .view() + + expect(balance.toNumber()).toBe(30000) + + balance = await program.methods + .getBalance(otherTokenAccount.address) + .accounts({ dataAccount: storage.publicKey }) + .remainingAccounts([ + { + pubkey: otherTokenAccount.address, + isSigner: false, + isWritable: false, + }, + ]) + .view() + + expect(balance.toNumber()).toBe(50000) + }) + }) +}) diff --git a/cross-chain/solana/package.json b/cross-chain/solana/package.json new file mode 100644 index 000000000..baf52087d --- /dev/null +++ b/cross-chain/solana/package.json @@ -0,0 +1,40 @@ +{ + "name": "solana-tests", + "version": "0.0.1", + "description": "Integration tests with Solang and Solana", + "scripts": { + "test": "tsc; ts-node setup.ts; mocha *.spec.ts", + "build": "solang compile *.sol --target solana -v", + "format": "npm run lint", + "format:fix": "npm run lint:fix", + "lint": "npm run lint:eslint", + "lint:fix": "npm run lint:fix:eslint", + "lint:eslint": "eslint .", + "lint:fix:eslint": "eslint . --fix" + }, + "author": "Sean Young ", + "license": "MIT", + "devDependencies": { + "@types/mocha": "^9.1.0", + "@types/node": "^14.14.10", + "expect": "^26.6.2", + "mocha": "^9.1.0", + "ts-node": "^10.4.0", + "typescript": "^4.1.2", + "prettier": "^2.5.1", + "eslint": "^7.32.0", + "@thesis-co/eslint-config": "github:thesis/eslint-config", + "prettier-plugin-sh": "^0.8.1", + "prettier-plugin-solidity": "^1.0.0-beta.19" + }, + "dependencies": { + "@project-serum/anchor": "^0.26", + "@solana/spl-token": "0.2.0", + "@solana/web3.js": "^1.68", + "ethers": "^5.2.0", + "fast-sha256": "^1.3.0", + "tweetnacl": "^1.0.3", + "web3-eth-abi": "^1.3.0", + "web3-utils": "^1.3.0" + } +} \ No newline at end of file diff --git a/cross-chain/solana/setup.ts b/cross-chain/solana/setup.ts new file mode 100644 index 000000000..a5b7acaf4 --- /dev/null +++ b/cross-chain/solana/setup.ts @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { + Connection, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + BpfLoader, + Transaction, + SystemProgram, + BPF_LOADER_PROGRAM_ID, + TransactionExpiredBlockheightExceededError, +} from "@solana/web3.js" +import { AnchorProvider, Program } from "@project-serum/anchor" +import fs from "fs" + +const endpoint: string = process.env.RPC_URL || "http://127.0.0.1:8899" + +export async function loadContract( + name: string, + args: any[] = [], + space = 8192 +): Promise<{ + program: Program + provider: AnchorProvider + storage: Keypair + programKey: PublicKey +}> { + const idl = JSON.parse(fs.readFileSync(`${name}.json`, "utf8")) + + process.env.ANCHOR_WALLET = "payer.key" + + const provider = AnchorProvider.local(endpoint) + + const storage = Keypair.generate() + + const programKey = loadKey(`${name}.key`) + + await createAccount(storage, programKey.publicKey, space) + + const program = new Program(idl, programKey.publicKey, provider) + + await program.methods + .new(...args) + .accounts({ dataAccount: storage.publicKey }) + .rpc() + + return { provider, program, storage, programKey: programKey.publicKey } +} + +export async function createAccount( + account: Keypair, + programId: PublicKey, + space: number +) { + const provider = AnchorProvider.local(endpoint) + const lamports = await provider.connection.getMinimumBalanceForRentExemption( + space + ) + + const transaction = new Transaction() + + transaction.add( + SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: account.publicKey, + lamports, + space, + programId, + }) + ) + + await provider.sendAndConfirm(transaction, [account]) +} + +export function newConnectionAndPayer(): [Connection, Keypair] { + const connection = newConnection() + const payerAccount = loadKey("payer.key") + return [connection, payerAccount] +} + +export async function loadContractWithProvider( + provider: AnchorProvider, + name: string, + args: any[] = [], + space = 8192 +): Promise<{ program: Program; storage: Keypair; programKey: PublicKey }> { + const idl = JSON.parse(fs.readFileSync(`${name}.json`, "utf8")) + + const storage = Keypair.generate() + const programKey = loadKey(`${name}.key`) + + await createAccount(storage, programKey.publicKey, space) + + const program = new Program(idl, programKey.publicKey, provider) + + await program.methods + .new(...args) + .accounts({ dataAccount: storage.publicKey }) + .rpc() + + return { program, storage, programKey: programKey.publicKey } +} + +export function loadKey(filename: string): Keypair { + const contents = fs.readFileSync(filename).toString() + const bs = Uint8Array.from(JSON.parse(contents)) + + return Keypair.fromSecretKey(bs) +} + +async function newAccountWithLamports( + connection: Connection +): Promise { + const account = Keypair.generate() + + console.log("Airdropping SOL to a new wallet ...") + const signature = await connection.requestAirdrop( + account.publicKey, + 100 * LAMPORTS_PER_SOL + ) + const latestBlockHash = await connection.getLatestBlockhash() + + await connection.confirmTransaction( + { + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature, + }, + "confirmed" + ) + + return account +} + +async function setup() { + const writeKey = (file_name: string, key: Keypair) => { + fs.writeFileSync(file_name, JSON.stringify(Array.from(key.secretKey))) + } + + let connection = newConnection() + const payer = await newAccountWithLamports(connection) + const thirdParty = await newAccountWithLamports(connection) + + writeKey("payer.key", payer) + writeKey("thirdParty.key", thirdParty) + + const files = fs.readdirSync(__dirname) + for (const index in files) { + const file = files[index] + + if (file.endsWith(".so")) { + const name = file.slice(0, -3) + let program + + if (fs.existsSync(`${name}.key`)) { + program = loadKey(`${name}.key`) + } else { + program = Keypair.generate() + } + + console.log(`Loading ${name} at ${program.publicKey}...`) + const programSo = fs.readFileSync(file) + for (let retries = 5; retries > 0; retries -= 1) { + try { + await BpfLoader.load( + connection, + payer, + program, + programSo, + BPF_LOADER_PROGRAM_ID + ) + break + } catch (e) { + if (e instanceof TransactionExpiredBlockheightExceededError) { + console.log(e) + console.log("retrying...") + connection = newConnection() + } else { + throw e + } + } + } + console.log(`Done loading ${name} ...`) + + writeKey(`${name}.key`, program) + } + } + + // If there was a TransactionExpiredBlockheightExceededError exception, then + // setup.ts does not exit. I have no idea why + process.exit() +} + +function newConnection(): Connection { + return new Connection(endpoint, { + commitment: "confirmed", + confirmTransactionInitialTimeout: 1e6, + }) +} + +if (require.main === module) { + (async () => { + await setup() + })() +} diff --git a/cross-chain/solana/solana-library/spl_token.sol b/cross-chain/solana/solana-library/spl_token.sol new file mode 100644 index 000000000..4f961fb29 --- /dev/null +++ b/cross-chain/solana/solana-library/spl_token.sol @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Disclaimer: This library provides a way for Solidity to interact with Solana's SPL-Token. Although it is production ready, +// it has not been audited for security, so use it at your own risk. + +import 'solana'; + +library SplToken { + address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; + enum TokenInstruction { + InitializeMint, // 0 + InitializeAccount, // 1 + InitializeMultisig, // 2 + Transfer, // 3 + Approve, // 4 + Revoke, // 5 + SetAuthority, // 6 + MintTo, // 7 + Burn, // 8 + CloseAccount, // 9 + FreezeAccount, // 10 + ThawAccount, // 11 + TransferChecked, // 12 + ApproveChecked, // 13 + MintToChecked, // 14 + BurnChecked, // 15 + InitializeAccount2, // 16 + SyncNative, // 17 + InitializeAccount3, // 18 + InitializeMultisig2, // 19 + InitializeMint2, // 20 + GetAccountDataSize, // 21 + InitializeImmutableOwner, // 22 + AmountToUiAmount, // 23 + UiAmountToAmount, // 24 + InitializeMintCloseAuthority, // 25 + TransferFeeExtension, // 26 + ConfidentialTransferExtension, // 27 + DefaultAccountStateExtension, // 28 + Reallocate, // 29 + MemoTransferExtension, // 30 + CreateNativeMint // 31 + } + + /// Mint new tokens. The transaction should be signed by the mint authority keypair + /// + /// @param mint the account of the mint + /// @param account the token account where the minted tokens should go + /// @param authority the public key of the mint authority + /// @param amount the amount of tokens to mint + function mint_to(address mint, address account, address authority, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.MintTo); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: mint, is_writable: true, is_signer: false}), + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: authority, is_writable: true, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Transfer @amount token from @from to @to. The transaction should be signed by the owner + /// keypair of the from account. + /// + /// @param from the account to transfer tokens from + /// @param to the account to transfer tokens to + /// @param owner the publickey of the from account owner keypair + /// @param amount the amount to transfer + function transfer(address from, address to, address owner, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.Transfer); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: from, is_writable: true, is_signer: false}), + AccountMeta({pubkey: to, is_writable: true, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: true, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Burn @amount tokens in account. This transaction should be signed by the owner. + /// + /// @param account the acount for which tokens should be burned + /// @param mint the mint for this token + /// @param owner the publickey of the account owner keypair + /// @param amount the amount to transfer + function burn(address account, address mint, address owner, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.Burn); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: mint, is_writable: true, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: true, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Approve an amount to a delegate. This transaction should be signed by the owner + /// + /// @param account the account for which a delegate should be approved + /// @param delegate the delegate publickey + /// @param owner the publickey of the account owner keypair + /// @param amount the amount to approve + function approve(address account, address delegate, address owner, uint64 amount) internal { + bytes instr = new bytes(9); + + instr[0] = uint8(TokenInstruction.Approve); + instr.writeUint64LE(amount, 1); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: delegate, is_writable: false, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: false, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Revoke a previously approved delegate. This transaction should be signed by the owner. After + /// this transaction, no delgate is approved for any amount. + /// + /// @param account the account for which a delegate should be approved + /// @param owner the publickey of the account owner keypair + function revoke(address account, address owner) internal { + bytes instr = new bytes(1); + + instr[0] = uint8(TokenInstruction.Revoke); + + AccountMeta[2] metas = [ + AccountMeta({pubkey: account, is_writable: true, is_signer: false}), + AccountMeta({pubkey: owner, is_writable: false, is_signer: true}) + ]; + + tokenProgramId.call{accounts: metas}(instr); + } + + /// Get the total supply for the mint, i.e. the total amount in circulation + /// @param mint the mint for this token + function total_supply(address mint) internal view returns (uint64) { + AccountInfo account = get_account_info(mint); + + return account.data.readUint64LE(36); + } + + /// Get the balance for an account. + /// + /// @param account the account for which we want to know a balance + function get_balance(address account) internal view returns (uint64) { + AccountInfo ai = get_account_info(account); + + return ai.data.readUint64LE(64); + } + + /// Get the account info for an account. This walks the transaction account infos + /// and find the account info, or the transaction fails. + /// + /// @param account the account for which we want to have the acount info. + function get_account_info(address account) internal view returns (AccountInfo) { + for (uint64 i = 0; i < tx.accounts.length; i++) { + AccountInfo ai = tx.accounts[i]; + if (ai.key == account) { + return ai; + } + } + + revert("account missing"); + } + + /// This enum represents the state of a token account + enum AccountState { + Uninitialized, + Initialized, + Frozen + } + + /// This struct is the return of 'get_token_account_data' + struct TokenAccountData { + address mintAccount; + address owner; + uint64 balance; + bool delegate_present; + address delegate; + AccountState state; + bool is_native_present; + uint64 is_native; + uint64 delegated_amount; + bool close_authority_present; + address close_authority; + } + + /// Fetch the owner, mint account and balance for an associated token account. + /// + /// @param tokenAccount The token account + /// @return struct TokenAccountData + function get_token_account_data(address tokenAccount) public view returns (TokenAccountData) { + AccountInfo ai = get_account_info(tokenAccount); + + TokenAccountData data = TokenAccountData( + { + mintAccount: ai.data.readAddress(0), + owner: ai.data.readAddress(32), + balance: ai.data.readUint64LE(64), + delegate_present: ai.data.readUint32LE(72) > 0, + delegate: ai.data.readAddress(76), + state: AccountState(ai.data[108]), + is_native_present: ai.data.readUint32LE(109) > 0, + is_native: ai.data.readUint64LE(113), + delegated_amount: ai.data.readUint64LE(121), + close_authority_present: ai.data.readUint32LE(129) > 10, + close_authority: ai.data.readAddress(133) + } + ); + + return data; + } + + // This struct is the return of 'get_mint_account_data' + struct MintAccountData { + bool authority_present; + address mint_authority; + uint64 supply; + uint8 decimals; + bool is_initialized; + bool freeze_authority_present; + address freeze_authority; + } + + /// Retrieve the information saved in a mint account + /// + /// @param mintAccount the account whose information we want to retrive + /// @return the MintAccountData struct + function get_mint_account_data(address mintAccount) public view returns (MintAccountData) { + AccountInfo ai = get_account_info(mintAccount); + + uint32 authority_present = ai.data.readUint32LE(0); + uint32 freeze_authority_present = ai.data.readUint32LE(46); + MintAccountData data = MintAccountData( { + authority_present: authority_present > 0, + mint_authority: ai.data.readAddress(4), + supply: ai.data.readUint64LE(36), + decimals: uint8(ai.data[44]), + is_initialized: ai.data[45] > 0, + freeze_authority_present: freeze_authority_present > 0, + freeze_authority: ai.data.readAddress(50) + }); + + return data; + } + + // A mint account has an authority, whose type is one of the members of this struct. + enum AuthorityType { + MintTokens, + FreezeAccount, + AccountOwner, + CloseAccount + } + + /// Remove the mint authority from a mint account + /// + /// @param mintAccount the public key for the mint account + /// @param mintAuthority the public for the mint authority + function remove_mint_authority(address mintAccount, address mintAuthority) public { + AccountMeta[2] metas = [ + AccountMeta({pubkey: mintAccount, is_signer: false, is_writable: true}), + AccountMeta({pubkey: mintAuthority, is_signer: true, is_writable: false}) + ]; + + bytes data = new bytes(9); + data[0] = uint8(TokenInstruction.SetAuthority); + data[1] = uint8(AuthorityType.MintTokens); + data[3] = 0; + + tokenProgramId.call{accounts: metas}(data); + } +} diff --git a/cross-chain/solana/solana-library/system_instruction.sol b/cross-chain/solana/solana-library/system_instruction.sol new file mode 100644 index 000000000..0ba370c8c --- /dev/null +++ b/cross-chain/solana/solana-library/system_instruction.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Disclaimer: This library provides a bridge for Solidity to interact with Solana's system instructions. Although it is production ready, +// it has not been audited for security, so use it at your own risk. + +import 'solana'; + +library SystemInstruction { + address constant systemAddress = address"11111111111111111111111111111111"; + address constant recentBlockHashes = address"SysvarRecentB1ockHashes11111111111111111111"; + address constant rentAddress = address"SysvarRent111111111111111111111111111111111"; + uint64 constant state_size = 80; + + enum Instruction { + CreateAccount, + Assign, + Transfer, + CreateAccountWithSeed, + AdvanceNounceAccount, + WithdrawNonceAccount, + InitializeNonceAccount, + AuthorizeNonceAccount, + Allocate, + AllocateWithSeed, + AssignWithSeed, + TransferWithSeed, + UpgradeNonceAccount // This is not available on Solana v1.9.15 + } + + /// Create a new account on Solana + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param to public key for the account to be created + /// @param lamports amount of lamports to be transfered to the new account + /// @param space the size in bytes that is going to be made available for the account + /// @param owner public key for the program that will own the account being created + function create_account(address from, address to, uint64 lamports, uint64 space, address owner) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: from, is_signer: true, is_writable: true}), + AccountMeta({pubkey: to, is_signer: true, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.CreateAccount), lamports, space, owner); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Create a new account on Solana using a public key derived from a seed + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param to the public key for the account to be created. The public key must match create_with_seed(base, seed, owner) + /// @param base the base address that derived the 'to' address using the seed + /// @param seed the string utilized to created the 'to' public key + /// @param lamports amount of lamports to be transfered to the new account + /// @param space the size in bytes that is going to be made available for the account + /// @param owner public key for the program that will own the account being created + function create_account_with_seed(address from, address to, address base, string seed, uint64 lamports, uint64 space, address owner) internal { + AccountMeta[3] metas = [ + AccountMeta({pubkey: from, is_signer: true, is_writable: true}), + AccountMeta({pubkey: to, is_signer: false, is_writable: true}), + AccountMeta({pubkey: base, is_signer: true, is_writable: false}) + ]; + + uint32 buffer_size = 92 + seed.length; + bytes bincode = new bytes(buffer_size); + bincode.writeUint32LE(uint32(Instruction.CreateAccountWithSeed), 0); + bincode.writeAddress(base, 4); + bincode.writeUint64LE(uint64(seed.length), 36); + bincode.writeString(seed, 44); + uint32 offset = seed.length + 44; + bincode.writeUint64LE(lamports, offset); + offset += 8; + bincode.writeUint64LE(space, offset); + offset += 8; + bincode.writeAddress(owner, offset); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Assign account to a program (owner) + /// + /// @param pubkey the public key for the account whose owner is going to be reassigned + /// @param owner the public key for the new account owner + function assign(address pubkey, address owner) internal { + AccountMeta[1] meta = [ + AccountMeta({pubkey: pubkey, is_signer: true, is_writable: true}) + ]; + bytes bincode = abi.encode(uint32(Instruction.Assign), owner); + + systemAddress.call{accounts: meta}(bincode); + } + + /// Assign account to a program (owner) based on a seed + /// + /// @param addr the public key for the account whose owner is going to be reassigned. The public key must match create_with_seed(base, seed, owner) + /// @param base the base address that derived the 'addr' key using the seed + /// @param seed the string utilized to created the 'addr' public key + /// @param owner the public key for the new program owner + function assign_with_seed(address addr, address base, string seed, address owner) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: addr, is_signer: false, is_writable: true}), + AccountMeta({pubkey: base, is_signer: true, is_writable: false}) + ]; + + + uint32 buffer_size = 76 + seed.length; + bytes bincode = new bytes(buffer_size); + bincode.writeUint32LE(uint32(Instruction.AssignWithSeed), 0); + bincode.writeAddress(base, 4); + bincode.writeUint64LE(uint64(seed.length), 36); + bincode.writeString(seed, 44); + bincode.writeAddress(owner, 44 + seed.length); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Transfer lamports between accounts + /// + /// @param from public key for the funding account + /// @param to public key for the recipient account + /// @param lamports amount of lamports to transfer + function transfer(address from, address to, uint64 lamports) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: from, is_signer: true, is_writable: true}), + AccountMeta({pubkey: to, is_signer: false, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.Transfer), lamports); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Transfer lamports from a derived address + /// + /// @param from_pubkey The funding account public key. It should match create_with_seed(from_base, seed, from_owner) + /// @param from_base the base address that derived the 'from_pubkey' key using the seed + /// @param seed the string utilized to create the 'from_pubkey' public key + /// @param from_owner owner to use to derive the funding account address + /// @param to_pubkey the public key for the recipient account + /// @param lamports amount of lamports to transfer + function transfer_with_seed(address from_pubkey, address from_base, string seed, address from_owner, address to_pubkey, uint64 lamports) internal { + AccountMeta[3] metas = [ + AccountMeta({pubkey: from_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: from_base, is_signer: true, is_writable: false}), + AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true}) + ]; + + uint32 buffer_size = seed.length + 52; + bytes bincode = new bytes(buffer_size); + bincode.writeUint32LE(uint32(Instruction.TransferWithSeed), 0); + bincode.writeUint64LE(lamports, 4); + bincode.writeUint64LE(seed.length, 12); + bincode.writeString(seed, 20); + bincode.writeAddress(from_owner, 20 + seed.length); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Allocate space in a (possibly new) account without funding + /// + /// @param pub_key account for which to allocate space + /// @param space number of bytes of memory to allocate + function allocate(address pub_key, uint64 space) internal { + AccountMeta[1] meta = [ + AccountMeta({pubkey: pub_key, is_signer: true, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.Allocate), space); + + systemAddress.call{accounts: meta}(bincode); + } + + /// Allocate space for an assign an account at an address derived from a base public key and a seed + /// + /// @param addr account for which to allocate space. It should match create_with_seed(base, seed, owner) + /// @param base the base address that derived the 'addr' key using the seed + /// @param seed the string utilized to create the 'addr' public key + /// @param space number of bytes of memory to allocate + /// @param owner owner to use to derive the 'addr' account address + function allocate_with_seed(address addr, address base, string seed, uint64 space, address owner) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: addr, is_signer: false, is_writable: true}), + AccountMeta({pubkey: base, is_signer: true, is_writable: false}) + ]; + + bytes bincode = new bytes(seed.length + 84); + bincode.writeUint32LE(uint32(Instruction.AllocateWithSeed), 0); + bincode.writeAddress(base, 4); + bincode.writeUint64LE(seed.length, 36); + bincode.writeString(seed, 44); + uint32 offset = 44 + seed.length; + bincode.writeUint64LE(space, offset); + offset += 8; + bincode.writeAddress(owner, offset); + + systemAddress.call{accounts: metas}(bincode); + } + + /// Create a new nonce account on Solana using a public key derived from a seed + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param nonce the public key for the account to be created. The public key must match create_with_seed(base, seed, systemAddress) + /// @param base the base address that derived the 'nonce' key using the seed + /// @param seed the string utilized to create the 'addr' public key + /// @param authority The entity authorized to execute nonce instructions on the account + /// @param lamports amount of lamports to be transfered to the new account + function create_nonce_account_with_seed(address from, address nonce, address base, string seed, address authority, uint64 lamports) internal { + create_account_with_seed(from, nonce, base, seed, lamports, state_size, systemAddress); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority); + systemAddress.call{accounts: metas}(bincode); + } + + /// Create a new account on Solana + /// + /// @param from public key for the account from which to transfer lamports to the new account + /// @param nonce the public key for the nonce account to be created + /// @param authority The entity authorized to execute nonce instructions on the account + /// @param lamports amount of lamports to be transfered to the new account + function create_nonce_account(address from, address nonce, address authority, uint64 lamports) internal { + create_account(from, nonce, lamports, state_size, systemAddress); + + AccountMeta[3] metas = [ + AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority); + systemAddress.call{accounts: metas}(bincode); + } + + /// Consumes a stored nonce, replacing it with a successor + /// + /// @param nonce_pubkey the public key for the nonce account + /// @param authorized_pubkey the publick key for the entity authorized to execute instructins on the account + function advance_nonce_account(address nonce_pubkey, address authorized_pubkey) internal { + AccountMeta[3] metas = [ + AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.AdvanceNounceAccount)); + systemAddress.call{accounts: metas}(bincode); + } + + /// Withdraw funds from a nonce account + /// + /// @param nonce_pubkey the public key for the nonce account + /// @param authorized_pubkey the public key for the entity authorized to execute instructins on the account + /// @param to_pubkey the recipient account + /// @param lamports the number of lamports to withdraw + function withdraw_nonce_account(address nonce_pubkey, address authorized_pubkey, address to_pubkey, uint64 lamports) internal { + AccountMeta[5] metas = [ + AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}), + AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}), + AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.WithdrawNonceAccount), lamports); + systemAddress.call{accounts: metas}(bincode); + } + + /// Change the entity authorized to execute nonce instructions on the account + /// + /// @param nonce_pubkey the public key for the nonce account + /// @param authorized_pubkey the public key for the entity authorized to execute instructins on the account + /// @param new_authority + function authorize_nonce_account(address nonce_pubkey, address authorized_pubkey, address new_authority) internal { + AccountMeta[2] metas = [ + AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}), + AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.AuthorizeNonceAccount), new_authority); + systemAddress.call{accounts: metas}(bincode); + } + + /// One-time idempotent upgrade of legacy nonce version in order to bump them out of chain domain. + /// + /// @param nonce the public key for the nonce account + // This is not available on Solana v1.9.15 + function upgrade_nonce_account(address nonce) internal { + AccountMeta[1] meta = [ + AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}) + ]; + + bytes bincode = abi.encode(uint32(Instruction.UpgradeNonceAccount)); + systemAddress.call{accounts: meta}(bincode); + } +} diff --git a/cross-chain/solana/tsconfig.json b/cross-chain/solana/tsconfig.json new file mode 100644 index 000000000..b9e8d9362 --- /dev/null +++ b/cross-chain/solana/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} \ No newline at end of file From 1afd0024117baa91b076ef4d22f216e6bccd685e Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Jul 2023 16:52:53 +0200 Subject: [PATCH 2/3] Adding README with some basic description --- cross-chain/solana/README.adoc | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 cross-chain/solana/README.adoc diff --git a/cross-chain/solana/README.adoc b/cross-chain/solana/README.adoc new file mode 100644 index 000000000..f425eb9c8 --- /dev/null +++ b/cross-chain/solana/README.adoc @@ -0,0 +1,56 @@ +:toc: macro + += Threshold cross-chain - Solana + +This package brings Bitcoin to Solana. For more details please +see link:https://github.com/keep-network/tbtc-v2/blob/main/docs/rfc/rfc-8.adoc[RFC 8: Cross-chain Tokenized Threshold BTC] + +== How it works? + +``` ++----------------------------+ +-----------------------------------------------------------------------+ +| Ethereum | | Solana | +| | | | +| +----------------------+ | | +----------------------+ +-----------------------+ +------------+ | +| | Wormhole TokenBridge |--|---------|--| Wormhole TokenBridge |--| SolanaWormholeGateway |--| SolanaTBTC | | +| +----------------------+ | | +----------------------+ +-----------------------+ +------------+ | +| | | | ++----------------------------+ +-----------------------------------------------------------------------+ +``` + +- `SolanaTBTC` canonical tBTC token on Solana with a minting authority +delegated to `SolanaWormholeGateway`. +- `SolanaWormholeGateway` is a smart contract wrapping and unwrapping +Wormhole-specific tBTC representation into the canonical `SolanaTBTC` token. + +=== Local development + +For testing and debugging purposes it is convinient to run Solana cluster locally. + +link:https://docs.solana.com/cli/install-solana-cli-tools#use-solanas-install-tool[Install] Solana Tool Suite + +In terminal run `solana-test-validator`. For more details see link:https://docs.solana.com/developing/test-validator[Solana Test Validator] + +==== Running tests + +Navigate to `/cross-chain/solana` and run `runTests.sh`. This script compiles Solidity contract(s) to produce Solana artifacts. + +=== Updating Wormhole Gateway mapping + +TODO: add + +=== Deploy contracts + +TODO: add + +=== Contract upgrades + +Supported out of the box. See https://docs.solana.com/cli/deploy-a-program#redeploy-a-program + +TODO: add examples + +=== Freezing and thawing token account + +Supported with a freezing authority passed as an argument during token creation. + +TODO: add examples \ No newline at end of file From 6364e4af442fb09a80fe147e08786fb34921c181 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 7 Jul 2023 16:53:49 +0200 Subject: [PATCH 3/3] Adding a script to compile SolanaTBTC contract and run the tests --- cross-chain/solana/runTests.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100755 cross-chain/solana/runTests.sh diff --git a/cross-chain/solana/runTests.sh b/cross-chain/solana/runTests.sh new file mode 100755 index 000000000..e0dd9b51d --- /dev/null +++ b/cross-chain/solana/runTests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +rm -rf SolanaTBTC.json +rm -rf SolanaTBTC.so + +solang compile -v --target solana SolanaTBTC.sol + +yarn test \ No newline at end of file