diff --git a/src/proto_alpha/lib_protocol/TEZOS_PROTOCOL b/src/proto_alpha/lib_protocol/TEZOS_PROTOCOL index 33e45da7c7789e77f15cb7dff28191a84b51af2b..cf6b1295d11c1a912b841b1d304a6afd3d54aa04 100644 --- a/src/proto_alpha/lib_protocol/TEZOS_PROTOCOL +++ b/src/proto_alpha/lib_protocol/TEZOS_PROTOCOL @@ -277,8 +277,8 @@ "Ticket_transfer", "Clst_storage", - "Script_native", "Script_interpreter_defs", + "Script_native", "Script_interpreter", "Sc_rollup_management_protocol", "Sc_rollup_operations", diff --git a/src/proto_alpha/lib_protocol/dune b/src/proto_alpha/lib_protocol/dune index b05f569013ca37032477e31aa02fcdce03fe26fa..e57a9089a61c1c127a6c5a2a470a8b3851c308b4 100644 --- a/src/proto_alpha/lib_protocol/dune +++ b/src/proto_alpha/lib_protocol/dune @@ -283,8 +283,8 @@ Ticket_accounting Ticket_transfer Clst_storage - Script_native Script_interpreter_defs + Script_native Script_interpreter Sc_rollup_management_protocol Sc_rollup_operations @@ -589,8 +589,8 @@ ticket_accounting.ml ticket_accounting.mli ticket_transfer.ml ticket_transfer.mli clst_storage.ml clst_storage.mli - script_native.ml script_native.mli script_interpreter_defs.ml + script_native.ml script_native.mli script_interpreter.ml script_interpreter.mli sc_rollup_management_protocol.ml sc_rollup_management_protocol.mli sc_rollup_operations.ml sc_rollup_operations.mli @@ -896,8 +896,8 @@ ticket_accounting.ml ticket_accounting.mli ticket_transfer.ml ticket_transfer.mli clst_storage.ml clst_storage.mli - script_native.ml script_native.mli script_interpreter_defs.ml + script_native.ml script_native.mli script_interpreter.ml script_interpreter.mli sc_rollup_management_protocol.ml sc_rollup_management_protocol.mli sc_rollup_operations.ml sc_rollup_operations.mli @@ -1187,8 +1187,8 @@ ticket_accounting.ml ticket_accounting.mli ticket_transfer.ml ticket_transfer.mli clst_storage.ml clst_storage.mli - script_native.ml script_native.mli script_interpreter_defs.ml + script_native.ml script_native.mli script_interpreter.ml script_interpreter.mli sc_rollup_management_protocol.ml sc_rollup_management_protocol.mli sc_rollup_operations.ml sc_rollup_operations.mli diff --git a/src/proto_alpha/lib_protocol/script_native.ml b/src/proto_alpha/lib_protocol/script_native.ml index 6f9d51e27df175c2e37dacf56337243db830084e..cb8d86006c4d65db2e557e6583303babe077408e 100644 --- a/src/proto_alpha/lib_protocol/script_native.ml +++ b/src/proto_alpha/lib_protocol/script_native.ml @@ -12,7 +12,12 @@ open Script_typed_ir module CLST_contract = struct open Script_native_types.CLST_types - type error += Empty_deposit | Non_implicit_contract of Destination.t + type error += + | Empty_transfer + | Non_empty_transfer of Destination.t * Tez.t + | Non_implicit_contract of Destination.t + | Balance_too_low of Destination.t * nat * nat + | Amount_too_large of Destination.t * nat let is_implicit : Destination.t -> bool = function | Destination.Contract (Contract.Implicit _) -> true @@ -22,7 +27,7 @@ module CLST_contract = struct (() : deposit) (storage : storage) : ((operation Script_list.t, storage) pair * context) tzresult Lwt.t = let open Lwt_result_syntax in - let*? () = error_when Tez.(step_constants.amount = zero) Empty_deposit in + let*? () = error_when Tez.(step_constants.amount = zero) Empty_transfer in let*? () = error_when (not (is_implicit step_constants.sender)) @@ -40,13 +45,75 @@ module CLST_contract = struct in let new_amount = Script_int.(add_n added_amount amount) in let* new_ledger, ctxt = - Script_big_map.update ctxt address (Some new_amount) storage + Clst_storage.set_balance_from_storage ctxt storage address new_amount in return ((Script_list.empty, new_ledger), ctxt) + let execute_withdraw (ctxt, (step_constants : Script_typed_ir.step_constants)) + (amount : withdraw) (storage : storage) : + ((operation Script_list.t, storage) pair * context) tzresult Lwt.t = + let open Lwt_result_syntax in + let*? () = + error_when + Tez.(step_constants.amount <> zero) + (Non_empty_transfer (step_constants.sender, step_constants.amount)) + in + let*? () = + error_when + Compare.Int.(Script_int.(compare zero_n amount) = 0) + Empty_transfer + in + let*? () = + error_when + Compare.Int.( + Script_int.(compare (of_int64 Int64.max_int) (int amount)) < 0) + (Amount_too_large (step_constants.sender, amount)) + in + let* typed_account = + match step_constants.sender with + | Contract (Implicit pkh) -> return (Typed_implicit pkh) + | sender -> tzfail (Non_implicit_contract sender) + in + let address = + {destination = step_constants.sender; entrypoint = Entrypoint.default} + in + let* current_amount, ctxt = + Clst_storage.get_balance_from_storage ctxt storage address + in + let* removed_amount = + if Compare.Int.(Script_int.compare current_amount amount < 0) then + tzfail (Balance_too_low (step_constants.sender, current_amount, amount)) + else return amount + in + let new_amount = Script_int.(abs (sub current_amount removed_amount)) in + let* new_ledger, ctxt = + Clst_storage.set_balance_from_storage ctxt storage address new_amount + in + let amount_tez = + Tez.of_mutez_exn + (Option.value ~default:0L (Script_int.to_int64 removed_amount)) + in + let gas_counter, outdated_ctxt = + Local_gas_counter.local_gas_counter_and_outdated_context ctxt + in + let* op, outdated_ctxt, gas_counter = + Script_interpreter_defs.transfer + (outdated_ctxt, step_constants) + gas_counter + amount_tez + Micheline.dummy_location + typed_account + () + in + let ctxt = Local_gas_counter.update_context gas_counter outdated_ctxt in + return ((Script_list.of_list [op], new_ledger), ctxt) + let execute (ctxt, (step_constants : step_constants)) (value : arg) (storage : storage) = - execute_deposit (ctxt, step_constants) value storage + match value with + | L () (* deposit *) -> execute_deposit (ctxt, step_constants) () storage + | R amount (* withdraw *) -> + execute_withdraw (ctxt, step_constants) amount storage end let execute (type arg storage) (ctxt, step_constants) @@ -59,14 +126,35 @@ let execute (type arg storage) (ctxt, step_constants) let () = register_error_kind `Branch - ~id:"clst.empty_deposit" - ~title:"Empty deposit" - ~description:"Forbidden to deposit 0ꜩ to CLST contract." + ~id:"clst.empty_transfer" + ~title:"Empty transfer" + ~description:"Forbidden to deposit or withdraw 0ꜩ on CLST contract." ~pp:(fun ppf () -> - Format.fprintf ppf "Deposit of 0ꜩ on CLST are forbidden.") + Format.fprintf ppf "Deposit or withdraw 0ꜩ on CLST are forbidden.") Data_encoding.unit - (function CLST_contract.Empty_deposit -> Some () | _ -> None) - (fun () -> CLST_contract.Empty_deposit) ; + (function CLST_contract.Empty_transfer -> Some () | _ -> None) + (fun () -> CLST_contract.Empty_transfer) ; + register_error_kind + `Branch + ~id:"clst.non_empty_transfer" + ~title:"Non empty transfer" + ~description:"Transferred amount is not used" + ~pp:(fun ppf (address, amount) -> + Format.fprintf + ppf + "Transferred amount %a from contract %a is not used" + Tez.pp + amount + Destination.pp + address) + Data_encoding.( + obj2 (req "address" Destination.encoding) (req "amount" Tez.encoding)) + (function + | CLST_contract.Non_empty_transfer (address, amount) -> + Some (address, amount) + | _ -> None) + (fun (address, amount) -> + CLST_contract.Non_empty_transfer (address, amount)) ; register_error_kind `Branch ~id:"clst.non_implicit_contract" @@ -81,4 +169,49 @@ let () = Data_encoding.(obj1 (req "address" Destination.encoding)) (function | CLST_contract.Non_implicit_contract address -> Some address | _ -> None) - (fun address -> CLST_contract.Non_implicit_contract address) + (fun address -> CLST_contract.Non_implicit_contract address) ; + register_error_kind + `Branch + ~id:"clst.balance_too_low" + ~title:"Balance is too low" + ~description:"Spending more clst tokens than the contract has" + ~pp:(fun ppf (address, balance, amount) -> + Format.fprintf + ppf + "Balance of contract %a too low (%s) to spend %s" + Destination.pp + address + (Script_int.to_string balance) + (Script_int.to_string amount)) + Data_encoding.( + obj3 + (req "address" Destination.encoding) + (req "balance" Script_int.n_encoding) + (req "amount" Script_int.n_encoding)) + (function + | CLST_contract.Balance_too_low (address, balance, amount) -> + Some (address, balance, amount) + | _ -> None) + (fun (address, balance, amount) -> + CLST_contract.Balance_too_low (address, balance, amount)) ; + register_error_kind + `Branch + ~id:"clst.amount_too_large" + ~title:"Amount is too large" + ~description:"Amount is too large for transfer" + ~pp:(fun ppf (address, amount) -> + Format.fprintf + ppf + "Amount %s is too large to transfer to contract %a" + (Script_int.to_string amount) + Destination.pp + address) + Data_encoding.( + obj2 + (req "address" Destination.encoding) + (req "amount" Script_int.n_encoding)) + (function + | CLST_contract.Amount_too_large (address, amount) -> + Some (address, amount) + | _ -> None) + (fun (address, amount) -> CLST_contract.Amount_too_large (address, amount)) diff --git a/src/proto_alpha/lib_protocol/script_native.mli b/src/proto_alpha/lib_protocol/script_native.mli index 7d13f79c491fad4a4055adf19e38932e7e66dcaf..bee2e761571e9c880f3a50d9b9606e0f613000e0 100644 --- a/src/proto_alpha/lib_protocol/script_native.mli +++ b/src/proto_alpha/lib_protocol/script_native.mli @@ -10,7 +10,12 @@ open Script_native_types open Script_typed_ir module CLST_contract : sig - type error += Empty_deposit | Non_implicit_contract of Destination.t + type error += + | Empty_transfer + | Non_empty_transfer of Destination.t * Tez.t + | Non_implicit_contract of Destination.t + | Balance_too_low of Destination.t * CLST_types.nat * CLST_types.nat + | Amount_too_large of Destination.t * CLST_types.nat end (* [execute ctxt kind arg storage] executes the given native contract [kind] diff --git a/src/proto_alpha/lib_protocol/script_native_types.ml b/src/proto_alpha/lib_protocol/script_native_types.ml index 9402d028ea0c6680c1914eb2bf077de2be8df87f..b1ef9ed15ee169bd5cfb0c133cbfae483cbcea7d 100644 --- a/src/proto_alpha/lib_protocol/script_native_types.ml +++ b/src/proto_alpha/lib_protocol/script_native_types.ml @@ -113,7 +113,9 @@ module CLST_types = struct type deposit = unit - type arg = deposit + type withdraw = nat + + type arg = (deposit, withdraw) or_ type ledger = (address, nat) big_map @@ -122,10 +124,15 @@ module CLST_types = struct let deposit_type : (deposit ty_node * deposit entrypoints_node) tzresult = make_entrypoint_leaf "deposit" (unit_ty ()) + let withdraw_type : (withdraw ty_node * withdraw entrypoints_node) tzresult = + make_entrypoint_leaf "withdraw" (nat_ty ()) + let arg_type : (arg ty_node * arg entrypoints) tzresult = let open Result_syntax in - let* deposit_type = deposit_type in - return (finalize_entrypoint deposit_type) + let* deposit_type in + let* withdraw_type in + let* arg_type = make_entrypoint_node deposit_type withdraw_type in + return (finalize_entrypoint arg_type) let storage_type : storage ty_node tzresult = address_big_map_ty (nat_ty ()) end diff --git a/src/proto_alpha/lib_protocol/script_native_types.mli b/src/proto_alpha/lib_protocol/script_native_types.mli index c53d5e19b273885b507651aa86da1a79bbb267ee..02c7161a625da5ed20a6810533e451e48dff238c 100644 --- a/src/proto_alpha/lib_protocol/script_native_types.mli +++ b/src/proto_alpha/lib_protocol/script_native_types.mli @@ -16,7 +16,9 @@ module CLST_types : sig type deposit = unit - type arg = deposit + type withdraw = nat + + type arg = (deposit, withdraw) or_ type ledger = (address, nat) big_map diff --git a/src/proto_alpha/lib_protocol/test/helpers/error_helpers.ml b/src/proto_alpha/lib_protocol/test/helpers/error_helpers.ml index 8cafc7ea18698bd9a2e4eb2fc7209700f10c81c3..902dec53e915dd0564d32eaaeee1c2d39c6c90bc 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/error_helpers.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/error_helpers.ml @@ -231,12 +231,21 @@ let missing_companion_key_for_bls_dal = function | Validate_errors.Consensus.Missing_companion_key_for_bls_dal _ -> true | _ -> false -let expect_clst_empty_deposit ~loc errs = +let expect_clst_empty_transfer ~loc errs = Assert.expect_error ~loc errs (function (* CLST is interacted with as a Michelson contract, as such the trace is always part of the interpreter error trace. *) | Script_interpreter.Runtime_contract_error _ - :: Script_native.CLST_contract.Empty_deposit :: _ -> + :: Script_native.CLST_contract.Empty_transfer :: _ -> + true + | _ -> false) + +let expect_clst_non_empty_transfer ~loc errs = + Assert.expect_error ~loc errs (function + (* CLST is interacted with as a Michelson contract, as such the trace is + always part of the interpreter error trace. *) + | Script_interpreter.Runtime_contract_error _ + :: Script_native.CLST_contract.Non_empty_transfer _ :: _ -> true | _ -> false) @@ -248,3 +257,12 @@ let expect_clst_non_implicit_depositer ~loc errs = :: Script_native.CLST_contract.Non_implicit_contract _ :: _ -> true | _ -> false) + +let expect_clst_balance_too_low ~loc errs = + Assert.expect_error ~loc errs (function + (* CLST is interacted with as a Michelson contract, as such the trace is + always part of the interpreter error trace. *) + | Script_interpreter.Runtime_contract_error _ + :: Script_native.CLST_contract.Balance_too_low _ :: _ -> + true + | _ -> false) diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.ml b/src/proto_alpha/lib_protocol/test/helpers/op.ml index 4ea36fc0b8426194d76da0229f3b6155d5ee0fdd..a2e032f1af94c8ffae5892e92be2f343ce7446e4 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.ml +++ b/src/proto_alpha/lib_protocol/test/helpers/op.ml @@ -1583,3 +1583,24 @@ let clst_deposit ?force_reveal ?counter ?fee ?gas_limit ?storage_limit src (Contract.Originated clst_hash) amount + +let clst_withdraw ?force_reveal ?counter ?fee ?gas_limit ?storage_limit + (ctxt : Context.t) (src : Contract.t) (amount : int64) = + let open Lwt_result_wrap_syntax in + let* alpha_ctxt = Context.get_alpha_ctxt ctxt in + let*@ clst_hash = Contract.get_clst_contract_hash alpha_ctxt in + let parameters = + Alpha_context.Script.lazy_expr (Expr.from_string (Int64.to_string amount)) + in + unsafe_transaction + ?force_reveal + ?counter + ?fee + ?gas_limit + ?storage_limit + ~entrypoint:(Entrypoint.of_string_strict_exn "withdraw") + ~parameters + ctxt + src + (Contract.Originated clst_hash) + Tez.zero diff --git a/src/proto_alpha/lib_protocol/test/helpers/op.mli b/src/proto_alpha/lib_protocol/test/helpers/op.mli index 03eaab56c058e96626adae81e33c659357a708b5..ed4e4c7039f48442b4188ee86f8a7c642d9b0812 100644 --- a/src/proto_alpha/lib_protocol/test/helpers/op.mli +++ b/src/proto_alpha/lib_protocol/test/helpers/op.mli @@ -917,3 +917,20 @@ val clst_deposit : Contract.t -> Tez.t -> Operation.packed tzresult Lwt.t + +(** [clst_withdraw ctxt src amount] returns a withdraw operation of + [amount] clst tokens on the CLST contract. + + [amount] is given in mutez, as the current internal exchange rate + between clst and mutez set at 1. This rate will be adjusted + soon. *) +val clst_withdraw : + ?force_reveal:bool -> + ?counter:Manager_counter.t -> + ?fee:Tez.t -> + ?gas_limit:gas_limit -> + ?storage_limit:Z.t -> + Context.t -> + Contract.t -> + int64 -> + Operation.packed tzresult Lwt.t diff --git a/src/proto_alpha/lib_protocol/test/integration/test_clst.ml b/src/proto_alpha/lib_protocol/test/integration/test_clst.ml index f3b3dbe2f163b852e00f75d01897115e8b7cd09f..1417a9df77d994cad046bec8e2ac2d61c8c938c6 100644 --- a/src/proto_alpha/lib_protocol/test/integration/test_clst.ml +++ b/src/proto_alpha/lib_protocol/test/integration/test_clst.ml @@ -28,24 +28,37 @@ let get_clst_hash ctxt = let*@ hash = Contract.get_clst_contract_hash alpha_ctxt in return hash -let test_deposit = - register_test ~title:"Test deposit of a non null amount" @@ fun () -> +let create_funded_account ~funder ~amount_mutez b = let open Lwt_result_wrap_syntax in - let* b, sender = Context.init1 () in - let amount = Tez.of_mutez_exn 100000000L in - let* deposit_tx = Op.clst_deposit (Context.B b) sender amount in - let* b = Block.bake ~operation:deposit_tx b in + let account = Account.new_account () in + let account = Contract.Implicit account.Account.pkh in + let* op = + Op.transaction (B b) funder account (Tez.of_mutez_exn amount_mutez) + in + let* b = Block.bake ~operation:op b in + return (account, b) + +let check_clst_balance_diff ~loc initial_balance_mutez diff_mutez b account = + let open Lwt_result_syntax in let* balance = - Plugin.Contract_services.clst_balance Block.rpc_ctxt b sender + Plugin.Contract_services.clst_balance Block.rpc_ctxt b account in - let amount = Tez.to_mutez amount in let balance = Option.value_f ~default:(fun () -> assert false) (Script_int.to_int64 balance) in - Check.((amount = balance) int64 ~error_msg:"Expected %L, got %R") ; - return_unit + let expected_balance = Int64.add initial_balance_mutez diff_mutez in + Assert.equal_int64 ~loc expected_balance balance + +let test_deposit = + register_test ~title:"Test deposit of a non null amount" @@ fun () -> + let open Lwt_result_wrap_syntax in + let* b, sender = Context.init1 () in + let amount = Tez.of_mutez_exn 100000000L in + let* deposit_tx = Op.clst_deposit (Context.B b) sender amount in + let* b = Block.bake ~operation:deposit_tx b in + check_clst_balance_diff ~loc:__LOC__ 0L (Tez.to_mutez amount) b sender let test_deposit_zero = register_test ~title:"Test depositing 0 tez amount is forbidden" @@ fun () -> @@ -57,7 +70,7 @@ let test_deposit_zero = match b with | Ok _ -> Test.fail "Empty deposits on CLST are forbidden and expected to fail" - | Error trace -> Error_helpers.expect_clst_empty_deposit ~loc:__LOC__ trace + | Error trace -> Error_helpers.expect_clst_empty_transfer ~loc:__LOC__ trace (* Contract taking a contract (or %) as parameter, and @@ -146,3 +159,124 @@ let test_deposit_from_originated_contract = "Deposits from smart contracts are forbidden and expected to fail" | Error trace -> Error_helpers.expect_clst_non_implicit_depositer ~loc:__LOC__ trace + +let () = + register_test ~title:"Test simple withdraw" @@ fun () -> + let open Lwt_result_wrap_syntax in + let* b, funder = Context.init1 ~consensus_threshold_size:0 () in + let initial_bal_mutez = 300_000_000L in + let* account, b = + create_funded_account ~funder ~amount_mutez:initial_bal_mutez b + in + let initial_clst_bal_mutez = 100_000_000L in + let* deposit_tx = + Op.clst_deposit + ~force_reveal:true + ~fee:Tez.zero + (B b) + account + (Tez.of_mutez_exn initial_clst_bal_mutez) + in + let* b = Block.bake ~operation:deposit_tx b in + let* () = + check_clst_balance_diff ~loc:__LOC__ 0L initial_clst_bal_mutez b account + in + + let* balance_before = Context.Contract.balance (B b) account in + let withdrawal_amount_mutez = 30_000_000L in + let* withdraw_tx = + Op.clst_withdraw ~fee:Tez.zero (B b) account withdrawal_amount_mutez + in + let* b = Block.bake ~operation:withdraw_tx b in + let* () = + Assert.balance_was_credited + ~loc:__LOC__ + (B b) + account + balance_before + (Tez.of_mutez_exn withdrawal_amount_mutez) + in + let* () = + check_clst_balance_diff + ~loc:__LOC__ + initial_clst_bal_mutez + (Int64.neg withdrawal_amount_mutez) + b + account + in + return_unit + +let () = + register_test ~title:"Test overwithdraw" @@ fun () -> + let open Lwt_result_wrap_syntax in + let* b, funder = Context.init1 ~consensus_threshold_size:0 () in + let initial_bal_mutez = 300_000_000L in + let* account, b = + create_funded_account ~funder ~amount_mutez:initial_bal_mutez b + in + let withdrawal_amount_mutez = 30_000_000L in + let* withdraw_tx = + Op.clst_withdraw + ~force_reveal:true + ~fee:Tez.zero + (B b) + account + withdrawal_amount_mutez + in + let*! b = Block.bake ~operation:withdraw_tx b in + match b with + | Ok _ -> + Test.fail + "Withdrawing more clst tokens than the contract has is forbidden" + | Error trace -> Error_helpers.expect_clst_balance_too_low ~loc:__LOC__ trace + +let () = + register_test ~title:"Test zero withdraw" @@ fun () -> + let open Lwt_result_wrap_syntax in + let* b, funder = Context.init1 ~consensus_threshold_size:0 () in + let initial_bal_mutez = 300_000_000L in + let* account, b = + create_funded_account ~funder ~amount_mutez:initial_bal_mutez b + in + let withdrawal_amount_mutez = 0L in + let* withdraw_tx = + Op.clst_withdraw + ~force_reveal:true + ~fee:Tez.zero + (B b) + account + withdrawal_amount_mutez + in + let*! b = Block.bake ~operation:withdraw_tx b in + match b with + | Ok _ -> Test.fail "Withdrawing 0 tez is forbidden" + | Error trace -> Error_helpers.expect_clst_empty_transfer ~loc:__LOC__ trace + +let () = + register_test ~title:"Test withdraw with non-zero transfer" @@ fun () -> + let open Lwt_result_wrap_syntax in + let* b, funder = Context.init1 ~consensus_threshold_size:0 () in + let initial_bal_mutez = 300_000_000L in + let* account, b = + create_funded_account ~funder ~amount_mutez:initial_bal_mutez b + in + let* clst_hash = get_clst_hash (Context.B b) in + let withdrawal_amount_mutez = 10L in + let* withdraw_tx = + Op.transaction + ~force_reveal:true + ~fee:Tez.zero + (B b) + account + (Contract.Originated clst_hash) + (Tez.of_mutez_exn 30_000L) + ~entrypoint:(Entrypoint.of_string_strict_exn "withdraw") + ~parameters: + (Alpha_context.Script.lazy_expr + (Expr.from_string (Int64.to_string withdrawal_amount_mutez))) + in + let*! b = Block.bake ~operation:withdraw_tx b in + match b with + | Ok _ -> Test.fail "Transferring to withdraw is forbidden" + | Error trace -> + Error_helpers.expect_clst_non_empty_transfer ~loc:__LOC__ trace