Browse Source

Add ability to pass seeds into external calls (#947)

Signed-off-by: Sean Young <sean@mess.org>
Co-authored-by: Lucas Steuernagel <lucas.tnagel@gmail.com>
Sean Young 3 years ago
parent
commit
bc7808f682

+ 44 - 0
docs/language/functions.rst

@@ -180,6 +180,50 @@ see the section on :ref:`account_meta`.
 
 If ``{accounts}`` is not specified, then all account are passed.
 
+Passing seeds with external calls on Solana
+___________________________________________
+
+The Solana runtime allows you to specify the seeds to be passed for an
+external call. This is used for program derived addresses: the seeds are
+hashed with the calling program id to create program derived addresses.
+They will automatically have the signer bit set, which allows a contract to
+sign without using any private keys.
+
+.. code-block:: solidity
+
+    import 'solana';
+
+    contract c {
+        address constant program_id = address"mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68";
+
+        function test(address addr, address addr2, bytes seed) public {
+            bytes instr = new bytes(1);
+
+            instr[0] = 1;
+
+            AccountMeta[2] metas = [
+                AccountMeta({pubkey: addr, is_writable: true, is_signer: true}),
+                AccountMeta({pubkey: addr2, is_writable: true, is_signer: true})
+            ];
+
+            token.call{accounts: metas, seeds: [ [ "test", seed ], [ "foo", "bar "] ]}(instr);
+        }
+    }
+
+Now if the program derived address for the running program id and the seeds match the address
+``addr`` and ``addr2``, then then the called program will run with signer and writable bits
+set for ``addr`` and ``addr2``. If they do not match, the Solana runtime will detect that
+the ``is_signer`` is set without the correct signature being provided.
+
+The seeds can provided in any other, which will be used to sign for multiple accounts. In the example
+above, the seed ``"test"`` is concatenated with the value of ``seed``, and that produces
+one account signature. In adition, ``"foo"`` is concatenated with ``"bar"`` to produce ``"foobar"``
+and then used to sign for another account.
+
+The ``seeds:`` call parameter is a slice of bytes slices; this means the literal can contain any
+number of elements, including 0 elements. The values can be ``bytes`` or anything that can be
+cast to ``bytes``.
+
 .. _passing_value_gas:
 
 Passing value and gas with external calls

+ 8 - 1
src/codegen/cfg.rs

@@ -124,6 +124,7 @@ pub enum Instr {
         success: Option<usize>,
         address: Option<Expression>,
         accounts: Option<Expression>,
+        seeds: Option<Expression>,
         payload: Expression,
         value: Expression,
         gas: Expression,
@@ -989,11 +990,12 @@ impl ControlFlowGraph {
                 payload,
                 value,
                 accounts,
+                seeds,
                 gas,
                 callty,
             } => {
                 format!(
-                    "{} = external call::{} address:{} payload:{} value:{} gas:{} accounts:{}",
+                    "{} = external call::{} address:{} payload:{} value:{} gas:{} accounts:{} seeds:{}",
                     match success {
                         Some(i) => format!("%{}", self.vars[i].id.name),
                         None => "_".to_string(),
@@ -1012,6 +1014,11 @@ impl ControlFlowGraph {
                     } else {
                         String::new()
                     },
+                    if let Some(seeds) = seeds {
+                        self.expr_to_string(contract, ns, seeds)
+                    } else {
+                        String::new()
+                    },
                 )
             }
             Instr::ValueTransfer {

+ 5 - 0
src/codegen/constant_folding.rs

@@ -227,6 +227,7 @@ pub fn constant_folding(cfg: &mut ControlFlowGraph, ns: &mut Namespace) {
                     value,
                     gas,
                     accounts,
+                    seeds,
                     callty,
                 } => {
                     let value = expression(value, Some(&vars), cfg, ns).0;
@@ -238,11 +239,15 @@ pub fn constant_folding(cfg: &mut ControlFlowGraph, ns: &mut Namespace) {
                     let accounts = accounts
                         .as_ref()
                         .map(|expr| expression(expr, Some(&vars), cfg, ns).0);
+                    let seeds = seeds
+                        .as_ref()
+                        .map(|expr| expression(expr, Some(&vars), cfg, ns).0);
 
                     cfg.blocks[block_no].instr[instr_no] = Instr::ExternalCall {
                         success: *success,
                         address,
                         accounts,
+                        seeds,
                         payload,
                         value,
                         gas,

+ 10 - 0
src/codegen/expression.rs

@@ -1213,6 +1213,7 @@ fn payable_send(
                 success: Some(success),
                 address: Some(address),
                 accounts: None,
+                seeds: None,
                 payload: Expression::AllocDynamicArray(
                     *loc,
                     Type::DynamicBytes,
@@ -1260,6 +1261,7 @@ fn payable_transfer(
             Instr::ExternalCall {
                 success: None,
                 accounts: None,
+                seeds: None,
                 address: Some(address),
                 payload: Expression::AllocDynamicArray(
                     *loc,
@@ -2170,6 +2172,11 @@ pub fn emit_function_call(
                 .as_ref()
                 .map(|expr| expression(expr, cfg, callee_contract_no, func, ns, vartab, opt));
 
+            let seeds = call_args
+                .seeds
+                .as_ref()
+                .map(|expr| expression(expr, cfg, callee_contract_no, func, ns, vartab, opt));
+
             let success = vartab.temp_name("success", &Type::Bool);
 
             let (payload, address) = if ns.target == Target::Solana && call_args.accounts.is_none()
@@ -2215,6 +2222,7 @@ pub fn emit_function_call(
                     value,
                     accounts,
                     gas,
+                    seeds,
                     callty: ty.clone(),
                 },
             );
@@ -2313,6 +2321,7 @@ pub fn emit_function_call(
                     Instr::ExternalCall {
                         success: None,
                         accounts: None,
+                        seeds: None,
                         address,
                         payload,
                         value,
@@ -2423,6 +2432,7 @@ pub fn emit_function_call(
                     Instr::ExternalCall {
                         success: None,
                         accounts: None,
+                        seeds: None,
                         address,
                         payload,
                         value,

+ 1 - 0
src/codegen/statements.rs

@@ -1143,6 +1143,7 @@ fn try_catch(
                         success: Some(success),
                         address: Some(address),
                         accounts: None,
+                        seeds: None,
                         payload,
                         value,
                         gas,

+ 16 - 1
src/codegen/subexpression_elimination/instruction.rs

@@ -112,11 +112,20 @@ impl AvailableExpressionSet {
                 payload,
                 value,
                 gas,
-                ..
+                accounts,
+                seeds,
+                callty: _,
+                success: _,
             } => {
                 if let Some(expr) = address {
                     let _ = self.gen_expression(expr, ave, cst);
                 }
+                if let Some(expr) = accounts {
+                    let _ = self.gen_expression(expr, ave, cst);
+                }
+                if let Some(expr) = seeds {
+                    let _ = self.gen_expression(expr, ave, cst);
+                }
                 let _ = self.gen_expression(payload, ave, cst);
                 let _ = self.gen_expression(value, ave, cst);
                 let _ = self.gen_expression(gas, ave, cst);
@@ -328,6 +337,7 @@ impl AvailableExpressionSet {
                 success,
                 address,
                 accounts,
+                seeds,
                 payload,
                 value,
                 gas,
@@ -341,10 +351,15 @@ impl AvailableExpressionSet {
                     .as_ref()
                     .map(|expr| self.regenerate_expression(expr, ave, cst).1);
 
+                let new_seeds = seeds
+                    .as_ref()
+                    .map(|expr| self.regenerate_expression(expr, ave, cst).1);
+
                 Instr::ExternalCall {
                     success: *success,
                     address: new_address,
                     accounts: new_accounts,
+                    seeds: new_seeds,
                     payload: self.regenerate_expression(payload, ave, cst).1,
                     value: self.regenerate_expression(value, ave, cst).1,
                     gas: self.regenerate_expression(gas, ave, cst).1,

+ 1 - 0
src/emit/ewasm.rs

@@ -1431,6 +1431,7 @@ impl<'a> TargetRuntime<'a> for EwasmTarget {
         gas: IntValue<'b>,
         value: IntValue<'b>,
         _accounts: Option<(PointerValue<'b>, IntValue<'b>)>,
+        _seeds: Option<(PointerValue<'b>, IntValue<'b>)>,
         callty: ast::CallTy,
         ns: &ast::Namespace,
     ) {

+ 81 - 5
src/emit/mod.rs

@@ -277,6 +277,7 @@ pub trait TargetRuntime<'a> {
         gas: IntValue<'b>,
         value: IntValue<'b>,
         accounts: Option<(PointerValue<'b>, IntValue<'b>)>,
+        seeds: Option<(PointerValue<'b>, IntValue<'b>)>,
         ty: CallTy,
         ns: &Namespace,
     );
@@ -4077,6 +4078,7 @@ pub trait TargetRuntime<'a> {
                         gas,
                         callty,
                         accounts,
+                        seeds,
                     } => {
                         let gas = self
                             .expression(bin, gas, &w.vars, function, ns)
@@ -4133,11 +4135,6 @@ pub trait TargetRuntime<'a> {
                             None
                         };
 
-                        let success = match success {
-                            Some(n) => Some(&mut w.vars.get_mut(n).unwrap().value),
-                            None => None,
-                        };
-
                         let (payload_ptr, payload_len) = if payload_ty == Type::DynamicBytes {
                             (bin.vector_bytes(payload), bin.vector_len(payload))
                         } else {
@@ -4152,6 +4149,84 @@ pub trait TargetRuntime<'a> {
                             (ptr, len)
                         };
 
+                        let seeds = if let Some(seeds) = seeds {
+                            let len = seeds.ty().array_length().unwrap().to_u64().unwrap();
+                            let seeds_ty = bin.llvm_type(
+                                &Type::Slice(Box::new(Type::Slice(Box::new(Type::Bytes(1))))),
+                                ns,
+                            );
+
+                            let output_seeds = bin.build_array_alloca(
+                                function,
+                                seeds_ty,
+                                bin.context.i64_type().const_int(len, false),
+                                "seeds",
+                            );
+
+                            if let Expression::ArrayLiteral(_, _, _, exprs) = seeds {
+                                for i in 0..len {
+                                    let val = self.expression(
+                                        bin,
+                                        &exprs[i as usize],
+                                        &w.vars,
+                                        function,
+                                        ns,
+                                    );
+
+                                    let seed_count = val
+                                        .get_type()
+                                        .into_pointer_type()
+                                        .get_element_type()
+                                        .into_array_type()
+                                        .len();
+
+                                    let dest = unsafe {
+                                        bin.builder.build_gep(
+                                            output_seeds,
+                                            &[
+                                                bin.context.i32_type().const_int(i, false),
+                                                bin.context.i32_type().const_zero(),
+                                            ],
+                                            "dest",
+                                        )
+                                    };
+
+                                    let val = bin.builder.build_pointer_cast(
+                                        val.into_pointer_value(),
+                                        dest.get_type().get_element_type().into_pointer_type(),
+                                        "seeds",
+                                    );
+
+                                    bin.builder.build_store(dest, val);
+
+                                    let dest = unsafe {
+                                        bin.builder.build_gep(
+                                            output_seeds,
+                                            &[
+                                                bin.context.i32_type().const_int(i, false),
+                                                bin.context.i32_type().const_int(1, false),
+                                            ],
+                                            "dest",
+                                        )
+                                    };
+
+                                    let val =
+                                        bin.context.i64_type().const_int(seed_count as u64, false);
+
+                                    bin.builder.build_store(dest, val);
+                                }
+                            }
+
+                            Some((output_seeds, bin.context.i64_type().const_int(len, false)))
+                        } else {
+                            None
+                        };
+
+                        let success = match success {
+                            Some(n) => Some(&mut w.vars.get_mut(n).unwrap().value),
+                            None => None,
+                        };
+
                         self.external_call(
                             bin,
                             function,
@@ -4162,6 +4237,7 @@ pub trait TargetRuntime<'a> {
                             gas,
                             value,
                             accounts,
+                            seeds,
                             callty.clone(),
                             ns,
                         );

+ 24 - 2
src/emit/solana.rs

@@ -3205,6 +3205,7 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
         _gas: IntValue<'b>,
         _value: IntValue<'b>,
         accounts: Option<(PointerValue<'b>, IntValue<'b>)>,
+        seeds: Option<(PointerValue<'b>, IntValue<'b>)>,
         _ty: ast::CallTy,
         _ns: &ast::Namespace,
     ) {
@@ -3320,8 +3321,29 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
 
             let external_call = binary.module.get_function("sol_invoke_signed_c").unwrap();
 
-            let signer_seeds = external_call.get_type().get_param_types()[3].const_zero();
-            let signer_seeds_len = external_call.get_type().get_param_types()[4].const_zero();
+            let (signer_seeds, signer_seeds_len) = if let Some((seeds, len)) = seeds {
+                (
+                    binary.builder.build_pointer_cast(
+                        seeds,
+                        external_call.get_type().get_param_types()[3].into_pointer_type(),
+                        "seeds",
+                    ),
+                    binary.builder.build_int_cast(
+                        len,
+                        external_call.get_type().get_param_types()[4].into_int_type(),
+                        "len",
+                    ),
+                )
+            } else {
+                (
+                    external_call.get_type().get_param_types()[3]
+                        .const_zero()
+                        .into_pointer_value(),
+                    external_call.get_type().get_param_types()[4]
+                        .const_zero()
+                        .into_int_value(),
+                )
+            };
 
             binary
                 .builder

+ 1 - 0
src/emit/substrate.rs

@@ -3634,6 +3634,7 @@ impl<'a> TargetRuntime<'a> for SubstrateTarget {
         gas: IntValue<'b>,
         value: IntValue<'b>,
         _accounts: Option<(PointerValue<'b>, IntValue<'b>)>,
+        _seeds: Option<(PointerValue<'b>, IntValue<'b>)>,
         _ty: ast::CallTy,
         ns: &ast::Namespace,
     ) {

+ 2 - 1
src/sema/ast.rs

@@ -760,13 +760,14 @@ pub enum Expression {
     List(pt::Loc, Vec<Expression>),
 }
 
-#[derive(PartialEq, Clone, Debug)]
+#[derive(PartialEq, Clone, Default, Debug)]
 pub struct CallArgs {
     pub gas: Option<Box<Expression>>,
     pub salt: Option<Box<Expression>>,
     pub value: Option<Box<Expression>>,
     pub space: Option<Box<Expression>>,
     pub accounts: Option<Box<Expression>>,
+    pub seeds: Option<Box<Expression>>,
 }
 
 impl Recurse for CallArgs {

+ 3 - 0
src/sema/dotgraphviz.rs

@@ -1194,6 +1194,9 @@ impl Dot {
         if let Some(accounts) = &call_args.accounts {
             self.add_expression(accounts, func, ns, node, String::from("accounts"));
         }
+        if let Some(seeds) = &call_args.seeds {
+            self.add_expression(seeds, func, ns, node, String::from("seeds"));
+        }
     }
 
     fn add_string_location(

+ 97 - 13
src/sema/expression.rs

@@ -6967,6 +6967,77 @@ fn array_literal(
     let mut dims = Box::new(Vec::new());
     let mut flattened = Vec::new();
 
+    let resolve_to = match resolve_to {
+        ResolveTo::Type(Type::Array(elem_ty, _)) => ResolveTo::Type(elem_ty),
+        // Solana seeds are a slice of slice of bytes, e.g. [ [ "fo", "o" ], [ "b", "a", "r"]]. In this
+        // case we want to resolve
+        ResolveTo::Type(Type::Slice(slice)) if matches!(slice.as_ref(), Type::Slice(_)) => {
+            let mut res = Vec::new();
+            let mut has_errors = false;
+
+            for expr in exprs {
+                let expr = match expression(
+                    expr,
+                    context,
+                    ns,
+                    symtable,
+                    diagnostics,
+                    ResolveTo::Type(&Type::Array(slice.clone(), vec![ArrayLength::Dynamic])),
+                ) {
+                    Ok(expr) => expr,
+                    Err(_) => {
+                        has_errors = true;
+                        continue;
+                    }
+                };
+
+                let ty = expr.ty();
+
+                if let Type::Array(elem, dims) = &ty {
+                    if elem != slice || dims.len() != 1 {
+                        diagnostics.push(Diagnostic::error(
+                            expr.loc(),
+                            format!(
+                                "type {} found where array {} expected",
+                                elem.to_string(ns),
+                                slice.to_string(ns)
+                            ),
+                        ));
+                        has_errors = true;
+                    }
+                } else {
+                    diagnostics.push(Diagnostic::error(
+                        expr.loc(),
+                        format!(
+                            "type {} found where array of slices expected",
+                            ty.to_string(ns)
+                        ),
+                    ));
+                    has_errors = true;
+                }
+
+                res.push(expr);
+            }
+
+            return if has_errors {
+                Err(())
+            } else {
+                let aty = Type::Array(
+                    slice.clone(),
+                    vec![ArrayLength::Fixed(BigInt::from(exprs.len()))],
+                );
+
+                Ok(Expression::ArrayLiteral(
+                    *loc,
+                    aty,
+                    vec![exprs.len() as u32],
+                    res,
+                ))
+            };
+        }
+        _ => resolve_to,
+    };
+
     check_subarrays(exprs, &mut Some(&mut dims), &mut flattened, diagnostics)?;
 
     if flattened.is_empty() {
@@ -6979,12 +7050,6 @@ fn array_literal(
 
     let mut flattened = flattened.iter();
 
-    let resolve_to = if let ResolveTo::Type(Type::Array(elem_ty, _)) = resolve_to {
-        ResolveTo::Type(elem_ty)
-    } else {
-        resolve_to
-    };
-
     // We follow the solidity scheme were everthing gets implicitly converted to the
     // type of the first element
     let mut first = expression(
@@ -7163,13 +7228,7 @@ fn parse_call_args(
         args.insert(&arg.name.name, arg);
     }
 
-    let mut res = CallArgs {
-        gas: None,
-        value: None,
-        salt: None,
-        space: None,
-        accounts: None,
-    };
+    let mut res = CallArgs::default();
 
     for arg in args.values() {
         match arg.name.name.as_str() {
@@ -7363,6 +7422,31 @@ fn parse_call_args(
 
                 res.accounts = Some(Box::new(expr));
             }
+            "seeds" => {
+                if ns.target != Target::Solana {
+                    diagnostics.push(Diagnostic::error(
+                        arg.loc,
+                        format!(
+                            "'seeds' not permitted for external calls or constructors on {}",
+                            ns.target
+                        ),
+                    ));
+                    return Err(());
+                }
+
+                let ty = Type::Slice(Box::new(Type::Slice(Box::new(Type::Bytes(1)))));
+
+                let expr = expression(
+                    &arg.expr,
+                    context,
+                    ns,
+                    symtable,
+                    diagnostics,
+                    ResolveTo::Type(&ty),
+                )?;
+
+                res.seeds = Some(Box::new(expr));
+            }
             _ => {
                 diagnostics.push(Diagnostic::error(
                     arg.loc,

+ 28 - 0
tests/contract_testcases/solana/bad_seeds_on_external_call.dot

@@ -0,0 +1,28 @@
+strict digraph "tests/contract_testcases/solana/bad_seeds_on_external_call.sol" {
+	contract [label="contract c\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:2:1-25:2"]
+	var [label="variable zero\nvisibility internal\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:4:2-27"]
+	number_literal [label="address literal: 0\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:4:17-27"]
+	test1 [label="function test1\ncontract: c\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:6:2-25\nsignature test1()\nvisibility public\nmutability nonpayable"]
+	diagnostic [label="found contract 'c'\nlevel Debug\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:2:1-25:2"]
+	diagnostic_7 [label="type bytes1 found where array of slices expected\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:13:4-7"]
+	diagnostic_8 [label="expected 'bytes1 slice[]', found integer\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:15:4-5"]
+	diagnostic_9 [label="type string found where array of slices expected\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:16:4-5"]
+	diagnostic_10 [label="type bytes2 found where array of slices expected\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:17:4-13"]
+	diagnostic_11 [label="type bytes found where array of slices expected\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:18:4-5"]
+	diagnostic_12 [label="expected 'bytes1 slice', found integer\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:20:5-6"]
+	diagnostic_13 [label="conversion from struct AccountMeta[1] to bytes1 slice not possible\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:21:5-10"]
+	diagnostic_14 [label="type bytes1 slice found where array bytes1 slice expected\nlevel Error\ntests/contract_testcases/solana/bad_seeds_on_external_call.sol:22:4-15"]
+	contracts -> contract
+	contract -> var [label="variable"]
+	var -> number_literal [label="initializer"]
+	contract -> test1 [label="function"]
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_7 [label="Error"]
+	diagnostics -> diagnostic_8 [label="Error"]
+	diagnostics -> diagnostic_9 [label="Error"]
+	diagnostics -> diagnostic_10 [label="Error"]
+	diagnostics -> diagnostic_11 [label="Error"]
+	diagnostics -> diagnostic_12 [label="Error"]
+	diagnostics -> diagnostic_13 [label="Error"]
+	diagnostics -> diagnostic_14 [label="Error"]
+}

+ 25 - 0
tests/contract_testcases/solana/bad_seeds_on_external_call.sol

@@ -0,0 +1,25 @@
+import "solana";
+
+contract c {
+	address zero = address(0);
+
+	function test1() public {
+		bytes instr = new bytes(0);
+		AccountMeta[1] metas;
+		string s = "abc";
+		bytes d = hex"abcd";
+
+		zero.call{accounts: metas, seeds: [
+			"a", // should be slice
+			[ "b"],  // ok
+			1, // should be slice
+			s, // should be slice
+			hex"f1f2", // should be slice
+			d, // should be slice
+			[d], // ok
+			[1], // not ok
+			[metas], // not ok
+			[ [ "a" ] ]  // should be slice
+		]}(instr);
+	}
+}

+ 8 - 4
tests/solana.rs

@@ -63,6 +63,10 @@ struct AccountState {
     lamports: u64,
 }
 
+/// We have a special callback function which tests that the correct
+/// parameters are passed in during CPI.
+type CallParametersCheck = fn(vm: &VirtualMachine, instr: &Instruction, pda: &[Pubkey]);
+
 struct VirtualMachine {
     account_data: HashMap<Account, AccountState>,
     origin: Account,
@@ -71,7 +75,7 @@ struct VirtualMachine {
     logs: String,
     events: Vec<Vec<Vec<u8>>>,
     return_data: Option<(Account, Vec<u8>)>,
-    call_test: HashMap<Pubkey, fn(instr: &Instruction)>,
+    call_params_check: HashMap<Pubkey, CallParametersCheck>,
 }
 
 #[derive(Clone)]
@@ -233,7 +237,7 @@ fn build_solidity(src: &str) -> VirtualMachine {
         logs: String::new(),
         events: Vec::new(),
         return_data: None,
-        call_test: HashMap::new(),
+        call_params_check: HashMap::new(),
     }
 }
 
@@ -1210,8 +1214,8 @@ impl<'a> SyscallObject<UserError> for SyscallInvokeSignedC<'a> {
 
             vm.return_data = None;
 
-            if let Some(handle) = vm.call_test.get(&instruction.program_id) {
-                handle(&instruction);
+            if let Some(handle) = vm.call_params_check.get(&instruction.program_id) {
+                handle(&vm, &instruction, &signers);
             } else if instruction.program_id.is_system_instruction() {
                 match bincode::deserialize::<u32>(&instruction.data).unwrap() {
                     0 => {

+ 51 - 3
tests/solana_tests/call.rs

@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: Apache-2.0
 
-use crate::{build_solidity, Instruction, Pubkey};
+use crate::{build_solidity, create_program_address, Instruction, Pubkey, VirtualMachine};
 use base58::FromBase58;
 use ethabi::{ethereum_types::U256, Token};
 
@@ -402,7 +402,7 @@ fn raw_call_accounts() {
             .unwrap(),
     );
 
-    let test_args = |instr: &Instruction| {
+    let test_args = |_vm: &VirtualMachine, instr: &Instruction, _signers: &[Pubkey]| {
         let sysvar_rent = Pubkey(
             "SysvarRent111111111111111111111111111111111"
                 .from_base58()
@@ -436,7 +436,7 @@ fn raw_call_accounts() {
         assert_eq!(instr.accounts[1].pubkey, sysvar_rent);
     };
 
-    vm.call_test.insert(token, test_args);
+    vm.call_params_check.insert(token, test_args);
 
     vm.function(
         "create_mint_with_freezeauthority",
@@ -449,3 +449,51 @@ fn raw_call_accounts() {
         None,
     );
 }
+
+#[test]
+fn pda() {
+    let mut vm = build_solidity(
+        r#"
+        import {AccountMeta} from 'solana';
+
+        contract pda {
+            address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
+            address constant SYSVAR_RENT_PUBKEY = address"SysvarRent111111111111111111111111111111111";
+
+            function test() public {
+                bytes instr = new bytes(1);
+
+                AccountMeta[1] metas = [
+                    AccountMeta({pubkey: SYSVAR_RENT_PUBKEY, is_writable: false, is_signer: false})
+                ];
+
+                tokenProgramId.call{seeds: [ ["foo"], ["b", "a", "r"] ], accounts: metas}(instr);
+            }
+        }"#,
+    );
+
+    vm.constructor("pda", &[]);
+
+    let test_args = |vm: &VirtualMachine, _instr: &Instruction, signers: &[Pubkey]| {
+        assert_eq!(
+            signers[0],
+            create_program_address(&vm.stack[0].program, &[b"foo"])
+        );
+        assert_eq!(
+            signers[1],
+            create_program_address(&vm.stack[0].program, &[b"bar"])
+        );
+    };
+
+    let token = Pubkey(
+        "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
+            .from_base58()
+            .unwrap()
+            .try_into()
+            .unwrap(),
+    );
+
+    vm.call_params_check.insert(token, test_args);
+
+    vm.function("test", &[], &[], None);
+}