Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support ticket #19

Merged
merged 13 commits into from
Feb 24, 2023
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ BUILT_APP_DIRECTORY:=$(BUILD_DIRECTORY)/$(APP_DIRECTORY)

# Ligo compiler
LIGO_COMPILER_ARGS:=--protocol kathmandu
LIGO_VERSION:=0.59.0
LIGO_VERSION:=0.60.0
LIGO?=ligo
LIGO_BUILD=$(LIGO) compile contract $(LIGO_COMPILER_ARGS)
LIGO_TEST=$(LIGO) run test
Expand Down Expand Up @@ -72,8 +72,8 @@ gen-wallet:

deploy:
$(eval SIGNER := $(shell TEZOS_CLIENT_UNSAFE_DISABLE_DISCLAIMER=yes ./_build/octez-client --endpoint https://ghostnet.tezos.marigold.dev show address wallet_address | grep Hash | awk '{print $$2}'))
$(BUILD_DIRECTORY)/octez-client --endpoint https://ghostnet.tezos.marigold.dev originate contract $(PROJECT_NAME)_unit transferring 2 from wallet_address running $(BUILT_APP_DIRECTORY)/$(PROJECT_NAME)_unit.tez --init '(Pair 0 {} {"$(SIGNER)";} 1 604800 {})' --burn-cap 2 -f
$(BUILD_DIRECTORY)/octez-client --endpoint https://ghostnet.tezos.marigold.dev originate contract $(PROJECT_NAME)_bytes transferring 2 from wallet_address running $(BUILT_APP_DIRECTORY)/$(PROJECT_NAME)_bytes.tez --init '(Pair 0 {} {"$(SIGNER)";} 1 604800 {})' --burn-cap 2 -f
$(BUILD_DIRECTORY)/octez-client --endpoint https://ghostnet.tezos.marigold.dev originate contract $(PROJECT_NAME)_unit transferring 2 from wallet_address running $(BUILT_APP_DIRECTORY)/$(PROJECT_NAME)_unit.tez --init '(Pair (Pair 0 {} {"$(SIGNER)";} 1 604800 {}) {})' --burn-cap 2 -f
$(BUILD_DIRECTORY)/octez-client --endpoint https://ghostnet.tezos.marigold.dev originate contract $(PROJECT_NAME)_bytes transferring 2 from wallet_address running $(BUILT_APP_DIRECTORY)/$(PROJECT_NAME)_bytes.tez --init '(Pair (Pair 0 {} {"$(SIGNER)";} 1 604800 {}) {})' --burn-cap 2 -f

get-tezos-binary:
wget -O $(BUILD_DIRECTORY)/octez-client $(TEZOS_BINARIES_URL)/octez-client
Expand Down
83 changes: 8 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,67 +7,6 @@ The contract is written in cameligo. Please follow [the instructions of installa
The minimal required version can be found by performing `make ligo-version`.

# Usage
## Create multisig wallet

To create a multisig wallet, you can use the default contracts that we provided or customize by yourself.

If purpose is to transfer Tez in and out only, the `app/main_unit.mligo` is recommended. Please run `make build` to compile the contract in Michelson which can be found in `_build/app/multisig_unit.tez`. See the Deploy section for the origination of the contract.

On the other hand, if executing other contracts is required, you can either use `app/main_bytes.mligo` or the multisig library to customize your own.

In the case of using `app/main_bytes.mligo`, you need to `pack` a parameter of the target contract as bytes and include it while creating a proposal. Once the minimal approvals are gathered, multisig will start executing the target contract. The target contract needs to `unpack` bytes by itself to get meaningful data.

```mermaid
sequenceDiagram
actor User
participant My Multisig Wallet
participant My Business Contract
Note right of My Business Contract: type parameter = Buy tez | Sell tez


User ->> User: params = pack (Buy 10tez)
User ->> My Multisig Wallet: create_proposal(..., params, ... )

Note over User,My Business Contract: gathering required approval

My Multisig Wallet ->> My Business Contract:execute(..., params, ...)
My Business Contract ->> My Business Contract: unpack(params), ...

```

Last, if you want a unpacked type, customizing your wallet is possible. First, in `package.json`, add `multi-signature` library in dependencies. In the library, we provide `contract` function with type variable `a`.

```
let contract (type a) (request : a parameter_types * a storage_types) : a result = ...
```
where `a` is the type of your contract parameter. You will need to use the function to define an entry of multisig. For example,

```ocaml
#import "ligo-multisig/src/lib.mligo" Multisig

type action
= Action1 of nat
| Action2 of string

(* your main entry *)
let main (input : action * storage) : operation list * storage =

(* the multisig entry *)
let multisig (input : action Multisig.parameter_types * action Multisig.storage_types) : action Multisig.result =
Multisig.contract input
```
Finally, compile the contracts with LIGO.

```bash
# compile your contract
ligo compile contract your_contract.mligo --entry-point main

# compile multisig contract
ligo compile contract your_contract.mligo --entry-point multisig
```

Note that the `Execute_lambda` is also provided another solutions for executing other contracts.

## Entrypoints of multisig

### default
Expand All @@ -76,15 +15,19 @@ This entrypoint can receive Tez from any source.
- tag: `default`
- data: `(sender, address)`

### ticket
This entrypoint can receive Tez from any source.
- Emit event
- tag: `ticket`
- data: `(sender, (ticketer, (payload, amount)))`

### create_proposal
Each owner can create proposal through this entrypoint. The entrypoint supports creating a batch of transactions. The batch is atomic and execution by order. If modifing settings are proposed, the modified setting will NOT apply in this batch immediately. The setting will effect on a next batch/transaction.

- proposal that owner can create
- `Transfer of { target:address; parameter:unit; amount:tez}`
- transfer amount only
- `Execute of { target:address; parameter:'a; amount:tez}`
- execute contract with type of parameter `'a`
- `Execute_lambda of { metadata: bytes option; lambda: (unit -> operation)}`
- `Execute_lambda of { metadata: bytes option; lambda: (('a ticket) option -> operation * ('a ticket) list) option; args: ('a * address * nat) option }`
- execute lambda, note that the cost of using `Transfer` and `Execute`is cheaper than `Execute_lambda`
- `Adjust_threshold of nat`
- adjust threshold. the threshold should be >0. Otherwises, errors will occur.
Expand All @@ -97,16 +40,7 @@ Each owner can create proposal through this entrypoint. The entrypoint supports
- tag: `create_proposal`
- data: `(proposal id, created proposal)`

### sign_and_resolve_proposal
Signers can provide an approval or a disapproval through this entrypoint. The owner who is statisfied the minimal requestment of approvals will also trigger the execution of proposal. After the proposal has been resolved, owners can not provide their approvals.

- Emit event
- tag: `sign_proposal`
- data: `(proposal id, owner, agreement)`
- tag: `resolve_proposal` (only when proposal is resolved)
- data: `(proposal id, owner)`

# sign_proposal_only
# sign_proposal
Signers can provide an approval or a disapproval through this entrypoint. Unlike `sign_and_resolve_proposal`, the proposal won't be resolve in any case.

- Emit event
Expand All @@ -131,4 +65,3 @@ We provide several steps for quick deploying contracts in `app/` to ghostnet.

# TODO
- support a different kind of threshold
- support FA2.1
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "multi-signature",
"version": "0.0.11",
"version": "0.0.12",
"author": "Marigold <[email protected]>",
"description": "Multi-signature wallet",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/common/errors.mligo
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ let amount_is_zero = "Amount should be greater than zero"
let not_the_same_content = "The proposal content doesn't match"
let invalid_effective_period = "Effective period should be greater than 0"
let pass_expiration_time = "The proposal has passed its expiration time"
let nonexisted_ticket = "Ticket is not found"
let cannot_happen = "cannot happen"
let balance_must_be_positive = "balance must be positive"
35 changes: 23 additions & 12 deletions src/internal/conditions.mligo
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
#import "storage.mligo" "Storage"

type storage_types = Storage.Types.t
type storage_wallet = Storage.Types.wallet
type storage_types_proposal = Storage.Types.proposal
type storage_types_proposal_state = Storage.Types.proposal_state
type effective_period = Storage.Types.effective_period
type proposal_content = Proposal_content.Types.t

[@inline]
let only_owner (type a) (storage : a storage_types) : unit =
assert_with_error (Set.mem (Tezos.get_sender ()) storage.owners) Errors.only_owner
let only_owner (type a) (wallet : a storage_wallet) : unit =
assert_with_error (Set.mem (Tezos.get_sender ()) wallet.owners) Errors.only_owner

[@inline]
let amount_must_be_zero_tez (an_amout : tez) : unit =
Expand All @@ -54,9 +55,13 @@ let check_proposal (type a) (content: a proposal_content) : unit =
match content with
| Transfer t ->
assert_with_error (not (t.amount = 0tez)) Errors.amount_is_zero
| Execute _ -> ()
| Execute_lambda e ->
assert_with_error (Util.is_some e.lambda) Errors.no_proposal
let () = assert_with_error (Util.is_some e.lambda) Errors.no_proposal in
begin
match e.args with
| None -> ()
| Some (_, _, v) -> assert_with_error (v > 0n) Errors.amount_is_zero
end
| Adjust_threshold t ->
assert_with_error (t > 0n) Errors.invalidated_threshold
| Add_owners s ->
Expand All @@ -72,19 +77,25 @@ let not_empty_content (type a) (proposals_content: (a proposal_content) list) :
List.iter check_proposal proposals_content

[@inline]
let check_setting (type a) (storage : a storage_types) : unit =
let () = assert_with_error (Set.cardinal storage.owners > 0n) Errors.no_owner in
let () = assert_with_error (Set.cardinal storage.owners >= storage.threshold) Errors.no_enough_owner in
let () = assert_with_error (storage.threshold > 0n) Errors.invalidated_threshold in
let () = assert_with_error (storage.effective_period > 0) Errors.invalid_effective_period in
let check_setting (type a) (wallet : a storage_wallet) : unit =
let () = assert_with_error (Set.cardinal wallet.owners > 0n) Errors.no_owner in
let () = assert_with_error (Set.cardinal wallet.owners >= wallet.threshold) Errors.no_enough_owner in
let () = assert_with_error (wallet.threshold > 0n) Errors.invalidated_threshold in
let () = assert_with_error (wallet.effective_period > 0) Errors.invalid_effective_period in
()

[@inline]
let check_proposals_content (type a) (from_input: (a proposal_content) list) (from_storage: (a proposal_content) list) : unit =
let check_proposals_content (type a) (from_input: (a proposal_content) list) (from_wallet: (a proposal_content) list) : unit =
let pack_from_input = Bytes.pack from_input in
let pack_from_storage = Bytes.pack from_storage in
assert_with_error (pack_from_input = pack_from_storage) Errors.not_the_same_content
let pack_from_wallet = Bytes.pack from_wallet in
assert_with_error (pack_from_input = pack_from_wallet) Errors.not_the_same_content

[@inline]
let within_expiration_time (created_timestamp: timestamp) (effective_period: effective_period) : unit =
assert_with_error (created_timestamp + effective_period > Tezos.get_now ()) Errors.pass_expiration_time

[@inline]
let balance_must_be_positive (target: nat) (balance: nat) : nat =
let balance = balance - target in
let _ = assert_with_error (balance >= 0) Errors.balance_must_be_positive in
abs(balance)
75 changes: 46 additions & 29 deletions src/internal/contract.mligo
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

type parameter_types = Parameter.Types.t
type storage_types = Storage.Types.t
type storage_wallet = Storage.Types.wallet
type storage_tickets = Storage.Types.tickets
type storage_types_proposal = Storage.Types.proposal
type storage_types_proposal_state = Storage.Types.proposal_state
type effective_period = Storage.Types.effective_period
Expand All @@ -39,76 +41,91 @@ type 'a result = operation list * 'a storage_types
(**
* Default entrypoint
*)
let default (type a) (_, s : unit * a storage_types) : a result =
let default (type a) (_, w, t : unit * a storage_wallet * a storage_tickets) : a result =
let event = Tezos.emit "%receiving_tez" (Tezos.get_sender (), Tezos.get_amount ()) in
([event], s)
([event], ({wallet = w; tickets = t} : a storage_types))

(**
* Ticket entrypoint
*)
let ticket (type a) (t, w, ts : a ticket * a storage_wallet * a storage_tickets) : a result =
let ticket_info,t = Tezos.read_ticket t in
let ts = Storage.Op.store_ticket (ts, t) in
let event = Tezos.emit "%receiving_ticket" (Tezos.get_sender (), ticket_info) in
([event], ({wallet = w; tickets = ts} : a storage_types))

(**
* Proposal creation
*)
let create_proposal (type a) (proposal_content, storage : (a proposal_content) list * a storage_types) : a result =
let () = Conditions.only_owner storage in
let create_proposal (type a) (proposal_content, wallet, tickets: (a proposal_content) list * a storage_wallet * a storage_tickets) : a result =
let () = Conditions.only_owner wallet in
let () = Conditions.amount_must_be_zero_tez (Tezos.get_amount ()) in
let () = Conditions.not_empty_content proposal_content in
let proposal = Storage.Op.create_proposal proposal_content in
let storage = Storage.Op.register_proposal(proposal, storage) in
let event = Tezos.emit "%create_proposal" (storage.proposal_counter, proposal) in
([event], storage)
let wallet = Storage.Op.register_proposal(proposal, wallet) in
let event = Tezos.emit "%create_proposal" (wallet.proposal_counter, proposal) in
([event], ({ wallet = wallet; tickets = tickets } : a storage_types))

(**
* Proposal signature only
*)

let sign_proposal (type a)
( proposal_id, proposal_content, agreement, storage
( proposal_id, proposal_content, agreement, wallet, tickets
: Parameter.Types.proposal_id
* (a proposal_content) list
* Parameter.Types.agreement
* a storage_types)
* a storage_wallet
* a storage_tickets)
: a result =
let () = Conditions.only_owner storage in
let proposal = Storage.Op.retrieve_proposal(proposal_id, storage) in
let () = Conditions.only_owner wallet in
let proposal = Storage.Op.retrieve_proposal(proposal_id, wallet) in
let () = Conditions.unresolved proposal.state in
let () = Conditions.unsigned proposal in
let () = Conditions.within_expiration_time proposal.proposer.timestamp storage.effective_period in
let () = Conditions.within_expiration_time proposal.proposer.timestamp wallet.effective_period in
let () = Conditions.check_proposals_content proposal_content proposal.contents in
let owner = Tezos.get_sender () in
let proposal = Storage.Op.update_signature (proposal, owner, agreement) in
let storage = Storage.Op.update_proposal(proposal_id, proposal, storage) in
let wallet = Storage.Op.update_proposal(proposal_id, proposal, wallet) in
let event = Tezos.emit "%sign_proposal" (proposal_id, owner, agreement) in
([event], storage)
([event], ({ wallet = wallet; tickets = tickets } : a storage_types))

(**
* Proposal Execution
*)

let resolve_proposal (type a)
( proposal_id, proposal_content, storage
( proposal_id, proposal_content, wallet, tickets
: Parameter.Types.proposal_id
* (a proposal_content) list
* a storage_types)
* a storage_wallet
* a storage_tickets)
: a result =
let () = Conditions.only_owner storage in
let proposal = Storage.Op.retrieve_proposal(proposal_id, storage) in
let () = Conditions.only_owner wallet in
let proposal = Storage.Op.retrieve_proposal(proposal_id, wallet) in
let () = Conditions.unresolved proposal.state in
let () = Conditions.check_proposals_content proposal_content proposal.contents in
let owner = Tezos.get_sender () in
let expiration_time = proposal.proposer.timestamp + storage.effective_period in
let proposal = Storage.Op.update_proposal_state (proposal, storage.owners, storage.threshold, expiration_time) in
let expiration_time = proposal.proposer.timestamp + wallet.effective_period in
let proposal = Storage.Op.update_proposal_state (proposal, wallet.owners, wallet.threshold, expiration_time) in
let () = Conditions.ready_to_execute proposal.state in
let storage = Storage.Op.update_proposal(proposal_id, proposal, storage) in
let ops, proposal, storage = Execution.perform_operations proposal storage in
let storage = Storage.Op.update_proposal(proposal_id, proposal, storage) in
let wallet = Storage.Op.update_proposal(proposal_id, proposal, wallet) in
let ops, proposal, wallet, tickets = Execution.perform_operations proposal wallet tickets in
let wallet = Storage.Op.update_proposal(proposal_id, proposal, wallet) in
let event = Tezos.emit "%resolve_proposal" (proposal_id, owner) in
(event::ops, storage)
(event::ops, ({ wallet = wallet; tickets = tickets } : a storage_types))

let contract (type a) (action, storage : a request) : a result =
let _ = Conditions.check_setting storage in
let { wallet; tickets } = storage in
let _ = Conditions.check_setting wallet in
match action with
| Default u -> default (u, storage)
| Default u ->
default (u, wallet, tickets)
| Ticket t ->
ticket (t, wallet, tickets)
| Create_proposal (proposal_params) ->
create_proposal (proposal_params, storage)
create_proposal (proposal_params, wallet, tickets)
| Sign_proposal (proposal_id, proposal_content, agreement) ->
sign_proposal (proposal_id, proposal_content, agreement, storage)
sign_proposal (proposal_id, proposal_content, agreement, wallet, tickets)
| Resolve_proposal (proposal_id, proposal_content) ->
resolve_proposal (proposal_id, proposal_content, storage)
resolve_proposal (proposal_id, proposal_content, wallet, tickets)
Loading