Browse Source

Fix incorrect deserialization (#1478)

The deserialization of `struct TokenAccountData` in the Solana library
is incorrect. This PR fixes the issue and tests the deserialization of
both `struct TokenAccountData` and `struct MintAccountData`.

---------

Signed-off-by: Lucas Steuernagel <lucas.tnagel@gmail.com>
Lucas Steuernagel 2 years ago
parent
commit
90c687ecb5

+ 11 - 0
integration/solana/account_data.sol

@@ -0,0 +1,11 @@
+import '../../solana-library/spl_token.sol';
+
+contract AccountData {
+    function token_account(address addr) view public returns (SplToken.TokenAccountData) {
+        return SplToken.get_token_account_data(addr);
+    }
+
+    function mint_account(address addr) view public returns (SplToken.MintAccountData) {
+        return SplToken.get_mint_account_data(addr);
+    }
+}

+ 243 - 0
integration/solana/account_data.spec.ts

@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: Apache-2.0
+
+
+import {loadContractAndCallConstructor, newConnectionAndPayer} from "./setup";
+import {Connection, Keypair, PublicKey} from "@solana/web3.js";
+import {
+    approveChecked,
+    AuthorityType,
+    createMint,
+    getAccount, getMint,
+    getOrCreateAssociatedTokenAccount, mintTo,
+    setAuthority
+} from "@solana/spl-token";
+import expect from "expect";
+import {Program} from "@coral-xyz/anchor";
+
+describe('Deserialize account data', function () {
+    this.timeout(500000);
+
+    let program: Program;
+    let storage: Keypair;
+    let connection: Connection;
+    let payer: Keypair;
+
+    before(async function (){
+        ({ program, storage } = await loadContractAndCallConstructor('AccountData'));
+        ([connection, payer] = newConnectionAndPayer()) ;
+    });
+
+    it('token account', async function check_token_account() {
+        const mint_authority = Keypair.generate();
+        const freeze_authority = Keypair.generate();
+
+        const mint = await createMint(
+            connection,
+            payer,
+            mint_authority.publicKey,
+            freeze_authority.publicKey,
+            0
+        );
+
+        const owner = Keypair.generate();
+
+        let token_account = await getOrCreateAssociatedTokenAccount(
+            connection,
+            payer,
+            mint,
+            owner.publicKey
+        );
+
+        let res = await program.methods.tokenAccount(token_account.address)
+            .accounts({dataAccount: storage.publicKey})
+            .remainingAccounts(
+                [
+                    {pubkey: token_account.address, isSigner: false, isWritable: false}
+                ]
+            )
+            .view();
+
+        expect(res.mintAccount).toEqual(token_account.mint);
+        expect(res.owner).toEqual(token_account.owner);
+        expect(res.balance.toString()).toEqual(token_account.amount.toString());
+        expect(res.delegatePresent).toEqual(false);
+        expect(res.delegatePresent).toEqual(token_account.delegate != null);
+        expect(res.delegate).toEqual(new PublicKey("11111111111111111111111111111111")); // 0
+        expect(res.state).toEqual({"initialized": {}});
+        expect(res.isNativePresent).toEqual(false);
+        expect(res.isNativePresent).toEqual(token_account.rentExemptReserve != null);
+        expect(res.isNative.toString()).toEqual("0");
+        expect(res.delegatedAmount.toString()).toEqual(token_account.delegatedAmount.toString());
+        expect(res.closeAuthorityPresent).toEqual(false);
+        expect(res.closeAuthorityPresent).toEqual(token_account.closeAuthority != null);
+        expect(res.closeAuthority).toEqual(new PublicKey("11111111111111111111111111111111")); // 0
+
+        const delegate_account = Keypair.generate();
+        // delegate tokens
+        await approveChecked(
+            connection,
+            payer,
+            mint,
+            token_account.address,
+            delegate_account.publicKey,
+            owner,
+            1,
+            0
+        );
+        token_account = await getAccount(connection, token_account.address);
+
+        res = await program.methods.tokenAccount(token_account.address)
+            .accounts({dataAccount: storage.publicKey})
+            .remainingAccounts(
+                [
+                    {pubkey: token_account.address, isSigner: false, isWritable: false}
+                ]
+            )
+            .view();
+
+        // The delegate account should be present now
+        expect(res.delegatePresent).toEqual(true);
+        expect(res.delegatePresent).toEqual(token_account.delegate !=  null);
+        expect(res.delegate).toEqual(token_account.delegate);
+
+        const close_authority = Keypair.generate();
+        // close authority
+        await setAuthority(
+            connection,
+            payer,
+            token_account.address,
+            owner,
+            AuthorityType.CloseAccount,
+            close_authority.publicKey
+        );
+        token_account = await getAccount(connection, token_account.address);
+
+        res = await program.methods.tokenAccount(token_account.address)
+            .accounts({dataAccount: storage.publicKey})
+            .remainingAccounts(
+                [
+                    {pubkey: token_account.address, isSigner: false, isWritable: false}
+                ]
+            )
+            .view();
+
+        // The close authority should be present
+        expect(res.closeAuthorityPresent).toEqual(true);
+        expect(res.closeAuthorityPresent).toEqual(token_account.closeAuthority != null);
+        expect(res.closeAuthority).toEqual(close_authority.publicKey);
+
+        const sol_mint = new PublicKey("So11111111111111111111111111111111111111112");
+        token_account = await getOrCreateAssociatedTokenAccount(
+            connection,
+            payer,
+            sol_mint,
+            owner.publicKey
+        );
+
+        res = await program.methods.tokenAccount(token_account.address)
+            .accounts({dataAccount: storage.publicKey})
+            .remainingAccounts(
+                [
+                    {pubkey: token_account.address, isSigner: false, isWritable: false}
+                ]
+            )
+            .view();
+
+        // Is native must be present
+        expect(res.isNativePresent).toEqual(token_account.isNative);
+        expect(res.isNativePresent).toEqual(true);
+        expect(res.isNativePresent).toEqual(token_account.rentExemptReserve != null);
+        expect(res.isNative.toString()).toEqual(token_account.rentExemptReserve!.toString());
+    });
+
+    it('mint account', async function mint_account() {
+        const mint_authority = Keypair.generate();
+        const freeze_authority = Keypair.generate();
+
+        const mint = await createMint(
+            connection,
+            payer,
+            mint_authority.publicKey,
+            freeze_authority.publicKey,
+            2
+        );
+
+        const owner = Keypair.generate();
+
+        const token_account = await getOrCreateAssociatedTokenAccount(
+            connection,
+            payer,
+            mint,
+            owner.publicKey
+        );
+
+        await mintTo(
+            connection,
+            payer,
+            mint,
+            token_account.address,
+            mint_authority,
+            5
+        );
+
+        let mint_data = await getMint(connection, mint);
+
+        let res = await program.methods.mintAccount(mint)
+            .accounts({dataAccount: storage.publicKey})
+            .remainingAccounts(
+                [
+                    {pubkey: mint, isWritable: false, isSigner: false}
+                ]
+            )
+            .view();
+
+        // Authorities are present
+        expect(res.authorityPresent).toEqual(true);
+        expect(res.authorityPresent).toEqual(mint_data.mintAuthority != null);
+        expect(res.mintAuthority).toEqual(mint_data.mintAuthority);
+        expect(res.supply.toString()).toEqual(mint_data.supply.toString())
+        expect(res.decimals).toEqual(mint_data.decimals);
+        expect(res.isInitialized).toEqual(mint_data.isInitialized);
+        expect(res.freezeAuthorityPresent).toEqual(true);
+        expect(res.freezeAuthorityPresent).toEqual(mint_data.freezeAuthority != null);
+        expect(res.freezeAuthority).toEqual(mint_data.freezeAuthority);
+
+        await setAuthority(
+            connection,
+            payer,
+            mint,
+            mint_authority,
+            AuthorityType.MintTokens,
+            null
+        );
+
+        await setAuthority(
+            connection,
+            payer,
+            mint,
+            freeze_authority,
+            AuthorityType.FreezeAccount,
+            null
+        );
+
+        mint_data = await getMint(connection, mint);
+
+        res = await program.methods.mintAccount(mint)
+            .accounts({dataAccount: storage.publicKey})
+            .remainingAccounts(
+                [
+                    {pubkey: mint, isWritable: false, isSigner: false}
+                ]
+            )
+            .view();
+
+        // Authorities are not present
+        expect(res.authorityPresent).toEqual(false);
+        expect(res.authorityPresent).toEqual(mint_data.mintAuthority != null);
+        expect(res.supply.toString()).toEqual(mint_data.supply.toString())
+        expect(res.decimals).toEqual(mint_data.decimals);
+        expect(res.isInitialized).toEqual(mint_data.isInitialized);
+        expect(res.freezeAuthorityPresent).toEqual(false);
+        expect(res.freezeAuthorityPresent).toEqual(mint_data.freezeAuthority != null);
+    });
+});

+ 1 - 1
solana-library/spl_token.sol

@@ -217,7 +217,7 @@ library SplToken {
 				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_present: ai.data.readUint32LE(129) > 0,
 				close_authority: ai.data.readAddress(133)
 			}
 		);

+ 18 - 12
tests/solana.rs

@@ -140,14 +140,24 @@ fn build_solidity(src: &str) -> VirtualMachine {
     VirtualMachineBuilder::new(src).build()
 }
 
-pub(crate) struct VirtualMachineBuilder<'a> {
-    src: &'a str,
+fn build_solidity_with_cache(cache: FileResolver) -> VirtualMachine {
+    VirtualMachineBuilder::new_with_cache(cache).build()
+}
+
+pub(crate) struct VirtualMachineBuilder {
+    cache: FileResolver,
     opts: Option<Options>,
 }
 
-impl<'a> VirtualMachineBuilder<'a> {
-    pub(crate) fn new(src: &'a str) -> Self {
-        Self { src, opts: None }
+impl VirtualMachineBuilder {
+    pub(crate) fn new(src: &str) -> Self {
+        let mut cache = FileResolver::default();
+        cache.set_file_contents("test.sol", src.to_string());
+        Self { cache, opts: None }
+    }
+
+    pub(crate) fn new_with_cache(cache: FileResolver) -> Self {
+        Self { cache, opts: None }
     }
 
     pub(crate) fn opts(mut self, opts: Options) -> Self {
@@ -155,14 +165,10 @@ impl<'a> VirtualMachineBuilder<'a> {
         self
     }
 
-    pub(crate) fn build(self) -> VirtualMachine {
-        let mut cache = FileResolver::default();
-
-        cache.set_file_contents("test.sol", self.src.to_string());
-
+    pub(crate) fn build(mut self) -> VirtualMachine {
         let (res, ns) = compile(
             OsStr::new("test.sol"),
-            &mut cache,
+            &mut self.cache,
             Target::Solana,
             self.opts.as_ref().unwrap_or(&Options {
                 opt_level: OptimizationLevel::Default,
@@ -175,7 +181,7 @@ impl<'a> VirtualMachineBuilder<'a> {
             "0.0.1",
         );
 
-        ns.print_diagnostics_in_plain(&cache, false);
+        ns.print_diagnostics_in_plain(&self.cache, false);
 
         assert!(!res.is_empty());
 

+ 293 - 1
tests/solana_tests/metas.rs

@@ -1,6 +1,12 @@
 // SPDX-License-Identifier: Apache-2.0
 
-use crate::{account_new, build_solidity, AccountMeta, AccountState, BorshToken, Pubkey};
+use crate::{
+    account_new, build_solidity, build_solidity_with_cache, AccountMeta, AccountState, BorshToken,
+    Pubkey,
+};
+use borsh::BorshSerialize;
+use num_bigint::BigInt;
+use solang::file_resolver::FileResolver;
 
 #[test]
 fn use_authority() {
@@ -65,3 +71,289 @@ fn use_authority() {
         }
     );
 }
+
+#[test]
+fn token_account() {
+    let mut cache = FileResolver::default();
+    cache.set_file_contents(
+        "spl_token.sol",
+        include_str!("../../solana-library/spl_token.sol").to_string(),
+    );
+    let src = r#"
+    import './spl_token.sol';
+
+contract Foo {
+    function token_account(address add) public returns (SplToken.TokenAccountData) {
+        return SplToken.get_token_account_data(add);
+    }
+}
+    "#;
+    cache.set_file_contents("test.sol", src.to_string());
+
+    let mut vm = build_solidity_with_cache(cache);
+
+    #[derive(BorshSerialize)]
+    struct TokenAccount {
+        mint_account: [u8; 32],
+        owner: [u8; 32],
+        balance: u64,
+        delegate_present: u32,
+        delegate: [u8; 32],
+        state: u8,
+        is_native_present: u32,
+        is_native: u64,
+        delegated_amount: u64,
+        close_authority_present: u32,
+        close_authority: [u8; 32],
+    }
+
+    let mut data = TokenAccount {
+        mint_account: account_new(),
+        owner: account_new(),
+        balance: 234,
+        delegate_present: 0,
+        delegate: account_new(),
+        state: 1,
+        is_native_present: 0,
+        is_native: 234,
+        delegated_amount: 1346,
+        close_authority_present: 0,
+        close_authority: account_new(),
+    };
+
+    let encoded = data.try_to_vec().unwrap();
+
+    let account = account_new();
+    vm.account_data.insert(
+        account,
+        AccountState {
+            owner: None,
+            lamports: 0,
+            data: encoded,
+        },
+    );
+
+    let data_account = vm.initialize_data_account();
+    vm.function("new")
+        .accounts(vec![("dataAccount", data_account)])
+        .call();
+
+    let res = vm
+        .function("token_account")
+        .arguments(&[BorshToken::Address(account)])
+        .accounts(vec![("dataAccount", data_account)])
+        .remaining_accounts(&[AccountMeta {
+            pubkey: Pubkey(account),
+            is_signer: false,
+            is_writable: false,
+        }])
+        .call()
+        .unwrap()
+        .unwrap_tuple();
+
+    assert_eq!(
+        res,
+        vec![
+            BorshToken::Address(data.mint_account),
+            BorshToken::Address(data.owner),
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.balance)
+            },
+            BorshToken::Bool(data.delegate_present > 0),
+            BorshToken::Address(data.delegate),
+            BorshToken::Uint {
+                width: 8,
+                value: BigInt::from(data.state)
+            },
+            BorshToken::Bool(data.is_native_present > 0),
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.is_native)
+            },
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.delegated_amount)
+            },
+            BorshToken::Bool(data.close_authority_present > 0),
+            BorshToken::Address(data.close_authority)
+        ]
+    );
+
+    data.delegate_present = 1;
+    data.is_native_present = 1;
+    data.close_authority_present = 1;
+
+    let encoded = data.try_to_vec().unwrap();
+    vm.account_data.get_mut(&account).unwrap().data = encoded;
+
+    let res = vm
+        .function("token_account")
+        .arguments(&[BorshToken::Address(account)])
+        .accounts(vec![("dataAccount", data_account)])
+        .remaining_accounts(&[AccountMeta {
+            pubkey: Pubkey(account),
+            is_signer: false,
+            is_writable: false,
+        }])
+        .call()
+        .unwrap()
+        .unwrap_tuple();
+
+    assert_eq!(
+        res,
+        vec![
+            BorshToken::Address(data.mint_account),
+            BorshToken::Address(data.owner),
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.balance)
+            },
+            BorshToken::Bool(data.delegate_present > 0),
+            BorshToken::Address(data.delegate),
+            BorshToken::Uint {
+                width: 8,
+                value: BigInt::from(data.state)
+            },
+            BorshToken::Bool(data.is_native_present > 0),
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.is_native)
+            },
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.delegated_amount)
+            },
+            BorshToken::Bool(data.close_authority_present > 0),
+            BorshToken::Address(data.close_authority)
+        ]
+    );
+}
+
+#[test]
+fn mint_account() {
+    let mut cache = FileResolver::default();
+    cache.set_file_contents(
+        "spl_token.sol",
+        include_str!("../../solana-library/spl_token.sol").to_string(),
+    );
+    let src = r#"
+    import './spl_token.sol';
+
+contract Foo {
+    function mint_account(address add) public returns (SplToken.MintAccountData) {
+        return SplToken.get_mint_account_data(add);
+    }
+}
+    "#;
+    cache.set_file_contents("test.sol", src.to_string());
+
+    let mut vm = build_solidity_with_cache(cache);
+
+    #[derive(BorshSerialize)]
+    struct MintAccountData {
+        authority_present: u32,
+        mint_authority: [u8; 32],
+        supply: u64,
+        decimals: u8,
+        is_initialized: bool,
+        freeze_authority_present: u32,
+        freeze_authority: [u8; 32],
+    }
+
+    let mut data = MintAccountData {
+        authority_present: 0,
+        mint_authority: account_new(),
+        supply: 450,
+        decimals: 4,
+        is_initialized: false,
+        freeze_authority_present: 0,
+        freeze_authority: account_new(),
+    };
+
+    let encoded = data.try_to_vec().unwrap();
+    let account = account_new();
+    vm.account_data.insert(
+        account,
+        AccountState {
+            owner: None,
+            lamports: 0,
+            data: encoded,
+        },
+    );
+
+    let data_account = vm.initialize_data_account();
+    vm.function("new")
+        .accounts(vec![("dataAccount", data_account)])
+        .call();
+
+    let res = vm
+        .function("mint_account")
+        .accounts(vec![("dataAccount", data_account)])
+        .arguments(&[BorshToken::Address(account)])
+        .remaining_accounts(&[AccountMeta {
+            pubkey: Pubkey(account),
+            is_writable: false,
+            is_signer: false,
+        }])
+        .call()
+        .unwrap()
+        .unwrap_tuple();
+
+    assert_eq!(
+        res,
+        vec![
+            BorshToken::Bool(data.authority_present > 0),
+            BorshToken::Address(data.mint_authority),
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.supply)
+            },
+            BorshToken::Uint {
+                width: 8,
+                value: BigInt::from(data.decimals)
+            },
+            BorshToken::Bool(data.is_initialized),
+            BorshToken::Bool(data.freeze_authority_present > 0),
+            BorshToken::Address(data.freeze_authority)
+        ]
+    );
+
+    data.authority_present = 1;
+    data.is_initialized = true;
+    data.freeze_authority_present = 1;
+    let encoded = data.try_to_vec().unwrap();
+    vm.account_data.get_mut(&account).unwrap().data = encoded;
+
+    let res = vm
+        .function("mint_account")
+        .accounts(vec![("dataAccount", data_account)])
+        .arguments(&[BorshToken::Address(account)])
+        .remaining_accounts(&[AccountMeta {
+            pubkey: Pubkey(account),
+            is_writable: false,
+            is_signer: false,
+        }])
+        .call()
+        .unwrap()
+        .unwrap_tuple();
+
+    assert_eq!(
+        res,
+        vec![
+            BorshToken::Bool(data.authority_present > 0),
+            BorshToken::Address(data.mint_authority),
+            BorshToken::Uint {
+                width: 64,
+                value: BigInt::from(data.supply)
+            },
+            BorshToken::Uint {
+                width: 8,
+                value: BigInt::from(data.decimals)
+            },
+            BorshToken::Bool(data.is_initialized),
+            BorshToken::Bool(data.freeze_authority_present > 0),
+            BorshToken::Address(data.freeze_authority)
+        ]
+    );
+}