├── .gitignore ├── bin └── ds-group ├── default.nix ├── Dappfile ├── libexec └── ds-group │ ├── ds-group-info │ ├── ds-group │ ├── ds-group-confirm │ ├── ds-group-propose │ ├── ds-group-help │ ├── ds-group-trigger │ ├── ds-group-action │ ├── ds-group-ls │ └── ds-group-verify ├── .gitmodules ├── Makefile ├── README.md └── src ├── group.sol └── group.t.sol /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | -------------------------------------------------------------------------------- /bin/ds-group: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | PATH=${0%/*/*}/libexec/ds-group:$PATH ds-group "$@" 3 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { solidityPackage, dappsys }: solidityPackage { 2 | name = "ds-multisig"; 3 | deps = with dappsys; [ds-exec ds-note ds-test]; 4 | src = ./src; 5 | } 6 | -------------------------------------------------------------------------------- /Dappfile: -------------------------------------------------------------------------------- 1 | name ds-group 2 | description Simple m-of-n multisig 3 | version 2 4 | 5 | author Ryan Casey 6 | author Daniel Brockman 7 | license Apache-2.0 8 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | values=$(seth call "${1?which group?}" "getInfo()(uint,uint,uint,uint)") 4 | values=($(seth --to-dec <<<"$values")) 5 | for name in quorum admins window actions; do 6 | printf "%-15s\t%s\n" "$name" "${values[$((i++))]}" 7 | done 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/ds-test"] 2 | path = lib/ds-test 3 | url = https://github.com/dapphub/ds-test 4 | [submodule "lib/ds-exec"] 5 | path = lib/ds-exec 6 | url = https://github.com/dapphub/ds-exec 7 | [submodule "lib/ds-note"] 8 | path = lib/ds-note 9 | url = https://github.com/dapphub/ds-note 10 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group -- command-line interface for DSGroup 3 | ### Usage: ds-group [] 4 | ### or: ds-group --help 5 | 6 | set -e 7 | 8 | if [[ $2 = --help ]]; then 9 | exec ds-group-help "$1" 10 | elif [[ ${1-help} = help ]]; then 11 | exec ds-group-"${1-help}" "${@:2}" 12 | fi 13 | 14 | ds-group-"$1" "${@:2}" 15 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-confirm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group-confirm -- confirm a proposed multisig action 3 | ### Usage: ds-group confirm 4 | set -e 5 | [[ $2 ]] || { ds-group confirm --help; exit 1; } 6 | 7 | fail() { echo >&2 "${0##*/}: $*"; exit 1; } 8 | echo "Confirming action ${2?which action?}..." 9 | 10 | seth send "${1?which group?}" "confirm(uint256)(bool)" "$2" || 11 | fail "error: failed to confirm action $2" 12 | 13 | echo "Successfully confirmed action $2." 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all:; dapp build 2 | test:; dapp test 3 | clean:; dapp clean 4 | 5 | SHELL = bash 6 | dirs = {bin,libexec} 7 | prefix ?= /usr/local 8 | 9 | dirs:; mkdir -p $(prefix)/$(dirs) 10 | files = $(shell ls -d $(dirs)/*) 11 | install:; cp -r -n $(dirs) $(prefix) 12 | link: uninstall dirs; for x in $(files); do \ 13 | ln -s `pwd`/$$x $(prefix)/$$x; done 14 | uninstall:; rm -rf $(addprefix $(prefix)/,$(files)) 15 | 16 | check:; ! grep '^#!/bin/sh' libexec/*/* && \ 17 | grep '^#!/usr/bin/env bash' libexec/*/* | \ 18 | cut -d: -f1 | xargs shellcheck -e SC1090 -e SC2018 -e SC2019 19 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-propose: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group-propose -- propose a new multisig action 3 | ### Usage: ds-group propose [] 4 | ### or: ds-group propose 5 | ### 6 | ### Submit a proposal for to call with and . 7 | ### If has the form `()', then infer from . 8 | set -e 9 | [[ $4 ]] || { ds-group propose --help; exit 1; } 10 | 11 | group=$(seth --to-address "${1?which group?}") 12 | target=$(seth --to-address "${2?which target?}") 13 | value=${3?how much ETH?} 14 | calldata=$(seth calldata "${@:4}") 15 | 16 | echo >&2 "Proposing action..." 17 | echo >&2 " target $target" 18 | echo >&2 " value $value" 19 | echo >&2 " calldata $calldata" 20 | 21 | sig="propose(address,bytes,uint256)(uint)" 22 | seth send "$group" "$sig" "${target#0x}" "${calldata#0x}" "$value" 23 | echo "Successfully proposed action." 24 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-help: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | name=${0##*/} name=${name%-help} file=${0%/*}/$name${1+-$1} 3 | header=$(perl -ne 'print "$2\n" if /^(\S)\1\1(?: (.*))?/' "$file") 4 | 5 | if ! [[ $header ]]; then 6 | sed "s/^/${0##*/}: /" "$file" 7 | elif [[ $(wc -l <<<"$header") = 1 ]]; then 8 | echo "$header" 9 | else 10 | sed 1d <<<"$header" 11 | fi 12 | 13 | if ! [[ $1 ]]; then 14 | for file in ${0%/*}/$name-*; do 15 | if [[ -L $file ]]; then 16 | continue 17 | else 18 | commands+=("$file") 19 | fi 20 | done 21 | 22 | list-commands() { 23 | cat "$@" | 24 | perl -ne 'print "$2 $3\n" if /^(\S)\1\1 '"$name"'-(\S+) -- (.*)/' | 25 | while read -r name label; do 26 | printf " %-12s %s\n" "$name" "$label" 27 | done | LANG=C sort 28 | } 29 | 30 | cat <<. 31 | 32 | Commands: 33 | 34 | $(list-commands "${commands[@]}") 35 | 36 | Report bugs at . 37 | . 38 | fi 39 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-trigger: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group-trigger -- trigger a confirmed multisig action 3 | ### Usage: ds-group trigger 4 | set -e 5 | [[ $2 ]] || { ds-group trigger --help; exit 1; } 6 | 7 | fail() { echo >&2 "${0##*/}: $*"; exit 1; } 8 | 9 | group=${1?which group?} 10 | id=${2?which action?} 11 | 12 | info=$(ds-group action "$group" "$id") 13 | get() { seth --field "$1" <<<"$info"; } 14 | 15 | if [[ $(get expired) = true ]]; then 16 | fail "error: action $id expired" 17 | elif [[ $(get confirmed) = false ]]; then 18 | fail "error: action $id not confirmed" 19 | elif [[ $(get triggered) = true ]]; then 20 | fail "error: action $id already triggered" 21 | fi 22 | 23 | echo "Triggering action $id..." 24 | seth send "$group" "trigger(uint)" "$id" 25 | 26 | info=$(ds-group action "$group" "$id") 27 | 28 | if [[ $(get triggered) = true ]]; then 29 | echo "Action $id successfully triggered." 30 | else 31 | fail "failed to trigger action $id" 32 | fi 33 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-action: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group-action -- print information about a multisig action 3 | ### Usage: ds-group action 4 | set -e 5 | [[ $2 ]] || { ds-group action --help; exit 1; } 6 | 7 | group=${1?which group?} 8 | action=${2?which action?} 9 | 10 | sig="getActionStatus(uint256)(uint256,uint256,bool,address,uint256)" 11 | values=($(seth call "$group" "$sig" "$action")) 12 | 13 | confirmations=$(seth --to-dec "${values[0]}") 14 | deadline=$(seth --to-dec "${values[1]}") 15 | triggered=${values[2]} 16 | 17 | target=$(seth --to-address "${values[3]}") 18 | value=$(seth --to-dec "${values[4]}") 19 | 20 | if (($(date +%s) >= deadline)); then 21 | expired=true 22 | else 23 | expired=false 24 | fi 25 | 26 | quorum=${DS_GROUP_QUORUM-$(ds-group info "$group" | seth --field quorum)} 27 | if ((confirmations >= quorum)); then 28 | confirmed=true 29 | else 30 | confirmed=false 31 | fi 32 | 33 | if [[ $triggered = true ]]; then 34 | status=Triggered 35 | elif [[ $expired = true ]]; then 36 | status=Expired 37 | elif [[ $confirmed = true ]]; then 38 | status=Untriggered 39 | else 40 | status=Unconfirmed 41 | fi 42 | 43 | : "$target" "$value" "$status" 44 | 45 | if [[ $deadline = 0 ]]; then 46 | echo >&2 "${0##*/}: error: no such action: $1" 47 | exit 1 48 | else 49 | properties=( 50 | confirmations 51 | confirmed 52 | deadline 53 | expired 54 | status 55 | target 56 | triggered 57 | value 58 | ) 59 | for name in "${properties[@]}"; do 60 | printf "%-15s\t%s\n" "$name" "${!name}" 61 | done 62 | fi 63 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-ls: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group-ls -- list already-proposed multisig actions 3 | ### Usage: ds-group ls [-a] [id1 id2 ...] 4 | set -e 5 | [[ $1 ]] || { ds-group ls --help; exit 1; } 6 | 7 | if [[ $1 == -a ]]; then all=1; shift; fi 8 | 9 | group=${1?which group?} 10 | info=$(ds-group info "$group") 11 | quorum=$(seth --field quorum <<<"$info") 12 | admins=$(seth --field admins <<<"$info") 13 | actions=$(seth --field actions <<<"$info") 14 | 15 | export DS_GROUP_QUORUM=$quorum 16 | 17 | ids="${*:2}" 18 | if [[ -z "$ids" ]] && [[ "$actions" -gt 0 ]]; then ids=$(seq "$actions"); else all=1; fi 19 | 20 | printf "%7s\t%14s\t%14s\t%s\n" \ 21 | ACTION CONFIRMATIONS EXPIRATION STATUS 22 | 23 | display-action() { 24 | action=$(ds-group action "$group" "$id") 25 | expired=$(seth --field expired <<<"$action") 26 | if [[ ! $all && $expired = true ]]; then return; fi 27 | deadline=$(seth --field deadline <<<"$action") 28 | confirmations=$(seth --field confirmations <<<"$action") 29 | status=$(seth --field status <<<"$action") 30 | printf "%7s\t%14s\t%14s\t%s\n" "$1" \ 31 | "$confirmations/$admins (need $quorum)" \ 32 | "$(pretty-date "$deadline")" "$status" 33 | } 34 | 35 | pretty-date() { 36 | if (($(date +%s) < $1)); then 37 | seconds=$(($1 - $(date +%s))) 38 | if ((seconds > 3600)); then 39 | echo "$((seconds / 60 / 60)) h left" 40 | else 41 | echo "$((seconds / 60)) min left" 42 | fi 43 | elif [[ $(date -d "@$1" +%Y) = $(date +%Y) ]]; then 44 | date -d "@$deadline" +"%b %d %H:%M" 45 | else 46 | date -d "@$deadline" +"%b %d %Y" 47 | fi 48 | } 49 | 50 | for id in $ids; do display-action "$id" & done | sort -n 51 | -------------------------------------------------------------------------------- /libexec/ds-group/ds-group-verify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ### ds-group-verify -- verify the meaning of a multisig action 3 | ### Usage: ds-group verify [] 4 | ### or: ds-group verify 5 | ### 6 | ### Verify proposing to call with and . 7 | ### If has the form `()', infer from . 8 | set -e 9 | [[ $5 ]] || { ds-group verify --help; exit 1; } 10 | 11 | fail() { echo >&2 "${0##*/}: $*"; exit 1; } 12 | 13 | group=${1?which group?} 14 | tx=${2?which transaction?} 15 | target=$(seth --to-address "${3?which target?}") 16 | target=${target#0x} 17 | value=${4?how much ETH?} 18 | calldata=$(seth calldata "${@:5}") 19 | calldata=${calldata#0x} 20 | inner_sig=${5?what calldata?} 21 | 22 | actual_group=$(seth tx "$tx" to) 23 | [[ $actual_group = "$group" ]] || fail "error: wrong group: $actual_group" 24 | 25 | actual=$(seth tx "$tx" input) 26 | actual=${actual#0x} 27 | 28 | sig="propose(address,bytes,uint256)" 29 | expected=$(seth calldata "$sig" "$target" "$calldata" "$value") 30 | expected=${expected#0x} 31 | 32 | explain() { 33 | hash=$(seth calldata "$1") 34 | hash=${hash#0x} 35 | if [[ $hash = '' ]]; then 36 | cat 37 | else 38 | sed "s/$hash/$hash # $1/" 39 | fi 40 | } 41 | 42 | format() { 43 | printf %s\\n "${1:0:8}" "${1:8:264}" "${1:272}" | fold -w64 | 44 | explain "$sig" | explain "$inner_sig" 45 | } 46 | 47 | actual=$(format "$actual") 48 | expected=$(format "$expected") 49 | 50 | diff -U9999 <(echo "$expected") <(echo "$actual") || 51 | fail "error: transaction does not match expected calldata" 52 | 53 | echo "$actual" 54 | echo "Transaction matches expected calldata." 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

DSGroup

2 | 3 | _Multisig with a command-line interface_ 4 | 5 | The ds-group library is `DSGroup` with a command-line interface. A list of 6 | `members`, the required `quorum` and the `window` of time in which actions 7 | must be approved are fixed when the `DSGroup` contract is created. Actions can 8 | then be proposed, confirmed and triggered once a group quorum has been reached. 9 | 10 | ## Installation & deployment 11 | 12 | The `DSGroup` contract takes three parameters: 13 | 14 | ``` 15 | function DSGroup(address[] members, uint quorum, uint window) 16 | ``` 17 | 18 | #### `address[] members` 19 | The list of group members. They will be able to create new proposals, accept them and trigger their execution. 20 | 21 | #### `uint quorum` 22 | The minimum number of members who have to accept a proposal before it can be triggered. 23 | 24 | #### `uint window` 25 | The proposal validity time in seconds. 26 | 27 | Install [Dapp](https://dapp.tools/dapp/) to build and deploy the contract: 28 | 29 | ```bash 30 | dapp build 31 | dapp create DSGroup '[ 32 | 0011111111111111111111111111111111111111, 33 | 0022222222222222222222222222222222222222, 34 | 0033333333333333333333333333333333333333 35 | ]' 2 86400 36 | ``` 37 | 38 | Install the [Seth](https://dapp.tools/seth/) dependency in order to use the 39 | command line interface. Then type `make link` from the ds-group directory 40 | to install the `ds-group` CLI tool: 41 | 42 | ```bash 43 | Usage: ds-group [] 44 | or: ds-group --help 45 | 46 | Commands: 47 | 48 | action print information about a multisig action 49 | confirm confirm a proposed multisig action 50 | ls list already-proposed multisig actions 51 | propose propose a new multisig action 52 | trigger trigger a confirmed multisig action 53 | verify verify the meaning of a multisig action 54 | ``` 55 | 56 | ### Examples 57 | 58 | ```bash 59 | ~$ ds-group ls @mkrgroup 60 | ACT CONFIRMATIONS EXPIRATION STATUS 61 | 15 0/6 (need 4) 8 h left Unconfirmed 62 | 16 0/6 (need 4) 9 h left Unconfirmed 63 | 64 | ~$ ds-group propose @mkrgroup @feedbase 0 "claim()" 65 | Proposing action... 66 | target 0x5927c5cc723c4486f93bf90bad3be8831139499e 67 | value 0 68 | calldata 0x4e71d92d 69 | seth-send: 0x307b667c434794c234b7c463b26827bdceb9c838fdb306f3f4398edefa5b1310 70 | seth-send: Waiting for transaction receipt......................... 71 | seth-send: Transaction included in block 1519991. 72 | seth-send: note: return value may be inaccurate (see `seth send --help') 73 | Successfully proposed act 17. 74 | 75 | ~$ ds-group ls @mkrgroup 76 | ACT CONFIRMATIONS EXPIRATION STATUS 77 | 15 0/6 (need 4) 8 h left Unconfirmed 78 | 16 0/6 (need 4) 9 h left Unconfirmed 79 | 17 0/6 (need 4) 23 h left Unconfirmed 80 | 81 | ~$ ds-group confirm @mkrgroup 17 82 | Confirming action 17... 83 | seth-send: 0x72fc6bf7c5135645a0fa298aa3ae01e072a82eabfddc8e3fbcdca72d0007d94b 84 | seth-send: Waiting for transaction receipt............... 85 | seth-send: Transaction included in block 1520018. 86 | 87 | ~$ ds-group ls @mkrgroup 88 | ACTION CONFIRMATIONS EXPIRATION STATUS 89 | 15 0/6 (need 4) 8 h left Unconfirmed 90 | 16 0/6 (need 4) 9 h left Unconfirmed 91 | 17 1/6 (need 4) 23 h left Unconfirmed 92 | 93 | ~$ ds-group trigger @mkrgroup 17 94 | ds-group-trigger: error: act not confirmed: 17 95 | 96 | ~$ ds-group action @mkrgroup 17 97 | calldata 0x4e71d92d 98 | confirmations 1 99 | confirmed false 100 | deadline 1471876934 101 | expired false 102 | status Unconfirmed 103 | target 0x5927c5cc723c4486f93bf90bad3be8831139499e 104 | triggered false 105 | value 0 106 | ``` 107 | -------------------------------------------------------------------------------- /src/group.sol: -------------------------------------------------------------------------------- 1 | /// group.sol -- simple m-of-n multisig implementation 2 | 3 | // Copyright (C) 2015, 2016 Ryan Casey 4 | // Copyright (C) 2016, 2017 Daniel Brockman 5 | 6 | // Licensed under the Apache License, Version 2.0 (the "License"). 7 | // You may not use this file except in compliance with the License. 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND (express or implied). 12 | 13 | pragma solidity ^0.4.11; 14 | 15 | import "ds-exec/exec.sol"; 16 | import "ds-note/note.sol"; 17 | 18 | contract DSGroup is DSExec, DSNote { 19 | address[] public members; 20 | uint public quorum; 21 | uint public window; 22 | uint public actionCount; 23 | 24 | mapping (uint => Action) public actions; 25 | mapping (uint => mapping (address => bool)) public confirmedBy; 26 | mapping (address => bool) public isMember; 27 | 28 | // Legacy events 29 | event Proposed (uint id, bytes calldata); 30 | event Confirmed (uint id, address member); 31 | event Triggered (uint id); 32 | 33 | struct Action { 34 | address target; 35 | bytes calldata; 36 | uint value; 37 | 38 | uint confirmations; 39 | uint deadline; 40 | bool triggered; 41 | } 42 | 43 | constructor( 44 | address[] members_, 45 | uint quorum_, 46 | uint window_ 47 | ) public { 48 | members = members_; 49 | quorum = quorum_; 50 | window = window_; 51 | 52 | for (uint i = 0; i < members.length; i++) { 53 | isMember[members[i]] = true; 54 | } 55 | } 56 | 57 | function memberCount() public view returns (uint) { 58 | return members.length; 59 | } 60 | 61 | function target(uint id) public view returns (address) { 62 | return actions[id].target; 63 | } 64 | function calldata(uint id) public view returns (bytes) { 65 | return actions[id].calldata; 66 | } 67 | function value(uint id) public view returns (uint) { 68 | return actions[id].value; 69 | } 70 | 71 | function confirmations(uint id) public view returns (uint) { 72 | return actions[id].confirmations; 73 | } 74 | function deadline(uint id) public view returns (uint) { 75 | return actions[id].deadline; 76 | } 77 | function triggered(uint id) public view returns (bool) { 78 | return actions[id].triggered; 79 | } 80 | 81 | function confirmed(uint id) public view returns (bool) { 82 | return confirmations(id) >= quorum; 83 | } 84 | function expired(uint id) public view returns (bool) { 85 | return now > deadline(id); 86 | } 87 | 88 | function deposit() public note payable { 89 | } 90 | 91 | function propose( 92 | address target, 93 | bytes calldata, 94 | uint value 95 | ) public onlyMembers note returns (uint id) { 96 | id = ++actionCount; 97 | 98 | actions[id].target = target; 99 | actions[id].calldata = calldata; 100 | actions[id].value = value; 101 | actions[id].deadline = now + window; 102 | 103 | emit Proposed(id, calldata); 104 | } 105 | 106 | function confirm(uint id) public onlyMembers onlyActive(id) note { 107 | assert(!confirmedBy[id][msg.sender]); 108 | 109 | confirmedBy[id][msg.sender] = true; 110 | actions[id].confirmations++; 111 | 112 | emit Confirmed(id, msg.sender); 113 | } 114 | 115 | function trigger(uint id) public onlyMembers onlyActive(id) note { 116 | assert(confirmed(id)); 117 | 118 | actions[id].triggered = true; 119 | exec(actions[id].target, actions[id].calldata, actions[id].value); 120 | 121 | emit Triggered(id); 122 | } 123 | 124 | modifier onlyMembers { 125 | assert(isMember[msg.sender]); 126 | _; 127 | } 128 | 129 | modifier onlyActive(uint id) { 130 | assert(!expired(id)); 131 | assert(!triggered(id)); 132 | _; 133 | } 134 | 135 | //------------------------------------------------------------------ 136 | // Legacy functions 137 | //------------------------------------------------------------------ 138 | 139 | function getInfo() public view returns ( 140 | uint quorum_, 141 | uint memberCount, 142 | uint window_, 143 | uint actionCount_ 144 | ) { 145 | return (quorum, members.length, window, actionCount); 146 | } 147 | 148 | function getActionStatus(uint id) public view returns ( 149 | uint confirmations, 150 | uint deadline, 151 | bool triggered, 152 | address target, 153 | uint value 154 | ) { 155 | return ( 156 | actions[id].confirmations, 157 | actions[id].deadline, 158 | actions[id].triggered, 159 | actions[id].target, 160 | actions[id].value 161 | ); 162 | } 163 | } 164 | 165 | contract DSGroupFactory is DSNote { 166 | mapping (address => bool) public isGroup; 167 | 168 | function newGroup( 169 | address[] members, 170 | uint quorum, 171 | uint window 172 | ) public note returns (DSGroup group) { 173 | group = new DSGroup(members, quorum, window); 174 | isGroup[group] = true; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/group.t.sol: -------------------------------------------------------------------------------- 1 | /// group.t.sol -- tests for group.sol 2 | 3 | // Copyright (C) 2015, 2016 Ryan Casey 4 | // Copyright (C) 2016, 2017 Daniel Brockman 5 | 6 | // Licensed under the Apache License, Version 2.0 (the "License"). 7 | // You may not use this file except in compliance with the License. 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND (express or implied). 12 | 13 | pragma solidity ^0.4.11; 14 | 15 | import "ds-test/test.sol"; 16 | 17 | import "./group.sol"; 18 | 19 | contract DSGroupTest is DSTest { 20 | DSGroupFactory factory; 21 | DSGroup group; 22 | Dummy dummy; 23 | Person alice; 24 | Person bob; 25 | Person eve; 26 | bytes calldata; 27 | 28 | function setUp() public { 29 | factory = new DSGroupFactory(); 30 | dummy = new Dummy(); 31 | alice = new Person(); 32 | bob = new Person(); 33 | eve = new Person(); 34 | 35 | address[] memory members = new address[](3); 36 | members[0] = alice; 37 | members[1] = bob; 38 | members[2] = this; 39 | 40 | group = factory.newGroup(members, 2, 3 days); 41 | } 42 | 43 | function test_setup() public { 44 | assertEq(group.members(0), alice); 45 | assertEq(group.members(1), bob); 46 | assertEq(group.members(2), this); 47 | 48 | assert(group.isMember(this)); 49 | assert(group.isMember(alice)); 50 | assert(group.isMember(bob)); 51 | assert(!group.isMember(eve)); 52 | 53 | assertEq(group.quorum(), 2); 54 | assertEq(group.memberCount(), 3); 55 | assertEq(group.window(), 3 days); 56 | assertEq(group.actionCount(), 0); 57 | 58 | var (quorum, memberCount, window, actionCount) = group.getInfo(); 59 | assertEq(quorum, 2); 60 | assertEq(memberCount, 3); 61 | assertEq(window, 3 days); 62 | assertEq(actionCount, 0); 63 | } 64 | 65 | function testFail_unconfirmed() public { 66 | var id = group.propose(dummy, new bytes(0), 0); 67 | group.trigger(id); 68 | } 69 | 70 | function testFail_transfer() public { 71 | var id = group.propose(dummy, new bytes(0), 123); 72 | group.confirm(id); 73 | bob.confirm(group, id); 74 | group.trigger(id); 75 | } 76 | 77 | function test_transfer() public { 78 | group.deposit.value(123)(); 79 | var id = group.propose(dummy, new bytes(0), 123); 80 | group.confirm(id); 81 | bob.confirm(group, id); 82 | group.trigger(id); 83 | assertEq(address(dummy).balance, 123); 84 | } 85 | 86 | function test_propose() public { 87 | assertEq(group.propose(dummy, new bytes(0), 0), 1); 88 | assertEq(group.actionCount(), 1); 89 | assertEq(group.target(1), dummy); 90 | assertEq(group.value(1), 0); 91 | assertEq(group.deadline(1), now + 3 days); 92 | assertEq(group.confirmations(1), 0); 93 | assert(!group.triggered(1)); 94 | assert(!group.expired(1)); 95 | assert(!group.confirmed(1)); 96 | assert(!group.confirmedBy(1, this)); 97 | assert(!group.confirmedBy(1, alice)); 98 | assert(!group.confirmedBy(1, bob)); 99 | assert(!group.confirmedBy(1, eve)); 100 | } 101 | 102 | function test_second_propose() public { 103 | assertEq(group.propose(dummy, new bytes(0), 0), 1); 104 | assertEq(group.propose(alice, new bytes(0), 0), 2); 105 | assertEq(group.actionCount(), 2); 106 | assertEq(group.target(1), dummy); 107 | assertEq(group.target(2), alice); 108 | } 109 | 110 | function test_confirm() public { 111 | group.propose(dummy, new bytes(0), 0); 112 | group.confirm(1); 113 | assertEq(group.confirmations(1), 1); 114 | assert(group.confirmedBy(1, this)); 115 | assert(!group.confirmedBy(1, alice)); 116 | assert(!group.confirmedBy(1, bob)); 117 | assert(!group.confirmedBy(1, eve)); 118 | assert(!group.confirmed(1)); 119 | assert(!group.triggered(1)); 120 | } 121 | 122 | function test_second_confirm() public { 123 | group.propose(dummy, new bytes(0), 0); 124 | group.confirm(1); 125 | alice.confirm(group, 1); 126 | assertEq(group.confirmations(1), 2); 127 | assert(group.confirmedBy(1, this)); 128 | assert(group.confirmedBy(1, alice)); 129 | assert(group.confirmed(1)); 130 | assert(!group.triggered(1)); 131 | } 132 | 133 | function test_third_confirm() public { 134 | group.propose(dummy, new bytes(0), 0); 135 | group.confirm(1); 136 | alice.confirm(group, 1); 137 | bob.confirm(group, 1); 138 | assertEq(group.confirmations(1), 3); 139 | assert(group.confirmedBy(1, bob)); 140 | assert(group.confirmed(1)); 141 | assert(!group.triggered(1)); 142 | } 143 | 144 | function testFail_double_confirm() public { 145 | group.propose(dummy, new bytes(0), 0); 146 | group.confirm(1); 147 | group.confirm(1); 148 | } 149 | 150 | function testFail_unauthorized_confirm() public { 151 | group.propose(dummy, new bytes(0), 0); 152 | eve.confirm(group, 1); 153 | } 154 | 155 | function testFail_premature_trigger() public { 156 | group.propose(dummy, new bytes(0), 0); 157 | group.confirm(1); 158 | group.trigger(1); 159 | } 160 | 161 | function test_trigger() public { 162 | group.propose(dummy, new bytes(0), 0); 163 | group.confirm(1); 164 | alice.confirm(group, 1); 165 | assert(!dummy.fallbackCalled()); 166 | group.trigger(1); 167 | assert(group.triggered(1)); 168 | assert(dummy.fallbackCalled()); 169 | } 170 | 171 | function test_failed_trigger() public { 172 | group.propose(dummy, new bytes(0), 0); 173 | group.confirm(1); 174 | alice.confirm(group, 1); 175 | dummy.setFallbackBlocked(true); 176 | assert(!group.call(bytes4(sha3("trigger(uint256)")), 1)); 177 | assert(!group.triggered(1)); 178 | assert(!dummy.fallbackCalled()); 179 | } 180 | 181 | function test_payment() public { 182 | group.deposit.value(500)(); 183 | assertEq(address(group).balance, 500); 184 | assertEq(address(dummy).balance, 0); 185 | 186 | group.propose(dummy, new bytes(0), 70); 187 | group.confirm(1); 188 | alice.confirm(group, 1); 189 | group.trigger(1); 190 | assertEq(address(dummy).balance, 70); 191 | assertEq(address(alice).balance, 0); 192 | assertEq(address(group).balance, 430); 193 | 194 | group.propose(alice, new bytes(0), 430); 195 | group.confirm(2); 196 | alice.confirm(group, 2); 197 | group.trigger(2); 198 | assertEq(address(dummy).balance, 70); 199 | assertEq(address(alice).balance, 430); 200 | assertEq(address(group).balance, 0); 201 | } 202 | 203 | function testFail_payment() public { 204 | assert(group.call.value(500)()); 205 | group.propose(dummy, new bytes(0), 501); 206 | group.confirm(0); 207 | alice.confirm(group, 0); 208 | group.trigger(0); 209 | } 210 | 211 | function test_calldata() public { 212 | bytes memory calldata = new bytes(4 + 32); 213 | bytes4 sig = bytes4(sha3("foo(uint256)")); 214 | 215 | calldata[0] = sig[0]; 216 | calldata[1] = sig[1]; 217 | calldata[2] = sig[2]; 218 | calldata[3] = sig[3]; 219 | calldata[4 + 31] = 123; 220 | 221 | group.propose(dummy, calldata, 0); 222 | group.confirm(1); 223 | alice.confirm(group, 1); 224 | group.trigger(1); 225 | assertEq(dummy.fooArgument(), 123); 226 | } 227 | } 228 | 229 | contract Dummy { 230 | uint public fooArgument; 231 | uint public fooValue; 232 | bool public fallbackBlocked; 233 | bool public fallbackCalled; 234 | 235 | function () payable public { 236 | if (fallbackBlocked) { 237 | throw; 238 | } else { 239 | fallbackCalled = true; 240 | } 241 | } 242 | 243 | function setFallbackBlocked(bool yes) public { 244 | fallbackBlocked = yes; 245 | } 246 | 247 | function foo(uint argument) payable public { 248 | fooArgument = argument; 249 | fooValue = msg.value; 250 | } 251 | } 252 | 253 | contract Person { 254 | function () payable public {} 255 | 256 | function confirm(DSGroup group, uint id) public { 257 | group.confirm(id); 258 | } 259 | } 260 | --------------------------------------------------------------------------------