1 of 47

Examples and Best Practices for Requestable Contracts�(The Hitchhiker’s Guide to Requestable Contracts)

박주형(Carl)

carl.p@onther.io �2019. 5. 27

<외부자료 활용 시 출처 명시 부탁드립니다.>

2 of 47

목차

  • Requestable Contracts
  • applyRequestIn(Root|Child)Chain functions
  • Requestable Contracts Examples and Best Practices
  • Make CryptoKitties Requestable

2

3 of 47

  • Requestable Contracts

3

4 of 47

1. Requestable Contracts (Enter)

4

5 of 47

5

6 of 47

2. applyRequestIn(Root|Child)Chain function

6

7 of 47

2. applyRequestIn(Root|Child)Chain

function applyRequestInRootChain(

bool isExit,

uint256 requestId,

address requestor,

bytes32 trieKey,

bytes trieValue

) external returns (bool success);

function applyRequestInChildChain(

bool isExit,

uint256 requestId,

address requestor,

bytes32 trieKey,

bytes trieValue

) external returns (bool success);

7

8 of 47

2. applyRequestIn(Root|Child)Chain

applyRequestInRootChain

  • Rootchain에 배포된 Requestable Contract에 Request를 반영하는 함수
  • RootChain 컨트랙트에 의해 실행

applyRequestInChildChain

  • Plasma chain에 배포된 Requestable Contract에 Request 를 반영하는 함수
  • Request Transaction에 의해 실행 (msg.sender == 0x00)

8

9 of 47

2. applyRequestIn(Root|Child)Chain

function applyRequestInChildChain(

bool isExit,

uint256 requestId,

address requestor,

bytes32 trieKey,

bytes trieValue

) external returns (bool success);

function applyRequestInRootChain(

bool isExit,

uint256 requestId,

address requestor,

bytes32 trieKey,

bytes trieValue

) external returns (bool success);

9

10 of 47

2. applyRequestIn(Root|Child)Chain

bytes32 trieKey

  • 어느 상태 변수에 대한 request인지 식별하기 위해 사용

bytes trieValue

  • 해당 Request가 변경하고자 하는 값
  • bytes 형이기에 별도의 decode 과정 필요
  • 1 word(32bytes) 이상의 데이터를 request 할 수 있다. → CryptoKitties?

10

11 of 47

3. Examples and Best Practices

11

12 of 47

3. Counter: BaseCounter

pragma solidity ^0.4.24;

contract BaseCounter {

uint n;

event Counted(uint _n);

function count() external {

n++;

emit Counted(n);

}

function getCount() external view returns (uint) {

return n;

}

}

12

BaseCounter 를 상속하여 다른 카운터 구현

13 of 47

3. Counter: SimpleCounter

13

14 of 47

3. Counter: SimpleCounter

function applyRequestInRootChain(...) … {

require(!appliedRequests[requestId]);

require(msg.sender == rootchain);

// only accept request for `n`.

require(trieKey == TRIE_KEY_N);

if (isExit) {

n = n.add(trieValue.toUint());

} else {

n = n.sub(trieValue.toUint());

}

appliedRequests[requestId] = true;

}

14

15 of 47

3. Counter: SimpleCounter

function applyRequestInChildChain(...) … {

require(!appliedRequests[requestId]);

require(msg.sender == address(0));

// only accept request for `n`.

require(trieKey == TRIE_KEY_N);

if (isExit) {

n = n.sub(trieValue.toUint());

} else {

n = n.add(trieValue.toUint());

}

appliedRequests[requestId] = true;

}

15

16 of 47

3. Counter: SimpleCounter

Rule 1. 한 체인에서 값을 증가시키면 다른 체인에서 그만큼 감소시키자.

Rule 2. Enter in RootChain과 Exit in ChildChain은 동일한 로직.�마찬가지로 Exit in RootChain과 Enter in ChildChain은 동일한 로직.

구현체 단점: 값이 감소하는 카운터가 감소하는 구현이 맞는걸까?

16

17 of 47

3. Counter: FreezableCounter

17

18 of 47

3. Counter: FreezableCounter

function applyRequestInRootChain(...) … {

...

require(freezed);

// only accept request for `n`.

require(trieKey == TRIE_KEY_N);

if (isExit) {

freezed = false;

n = trieValue.toUint();

} else {

require(n == trieValue.toUint());

}

...

}

18

19 of 47

3. Counter: FreezableCounter

function applyRequestInChildChain(...) … {

...

require(freezed);

// only accept request for `n`.

require(trieKey == TRIE_KEY_N);

if (isExit) {

require(n == trieValue.toUint());

} else {

n = trieValue.toUint();

freezed = false;

}

...

}

19

20 of 47

3. Counter: FreezableCounter

Rule 3. 값 일부가 아닌 전체를 Request 할 땐 증감(+,-)이 아닌 할당(=) 연산을 사용하자. 또한 증감 연산은 uint, int 자료형에만 사용하자.

구현체 단점: 도중에 두 컨트랙 모두 사용하지 못하는 경우가 올바른가?

20

21 of 47

3. Counter: FreezableCounter

21

22 of 47

3. Counter: TrackableCounter

/// @dev override BaseCounter.count function.

function count() external {

requestableN++;

n++;

emit Counted(n);

}

22

23 of 47

3. Counter: TrackableCounter

Simple Counter

23

24 of 47

3. Counter: TrackableCounter

function applyRequestInRootChain( ... ) ... {

require(!appliedRequests[requestId]);

require(msg.sender == rootchain);

// only accept request for `n`.

require(trieKey == TRIE_KEY_N);

uint _n = trieValue.toUint();

if (isExit) {

n = n.add(_n);

} else {

requestableN = requestableN.sub(_n);

}

appliedRequests[requestId] = true;

}

24

25 of 47

3. Counter: TrackableCounter

function applyRequestInChildChain( ... ) ... {

require(!appliedRequests[requestId]);

require(msg.sender == address(0));

// only accept request for `n`.

require(trieKey == TRIE_KEY_N);

uint _n = trieValue.toUint();

if (isExit) {

requestableN = requestableN.sub(_n);

} else {

n = n.add(_n);

}

appliedRequests[requestId] = true;

}

25

26 of 47

3. Counter: TrackableCounter

Tip 1.정 변수에 하나의 연산(e.g. +)만 사용하고 싶다면�Enter in RootChain과 Exit in ChildChain 에서 관여하는 변수와 �Enter in ChildChainExit in RootChain에서 관여하는 변수를 다르게 두자.�

  • Enter in RootChain, Exit in ChildChain : requestableN (-)
  • Enter in ChildChain, Exit in RootChain : n (+)
  • count() : requestableN, n (+)

26

27 of 47

function applyRequestInRootChain(...) ... {

if (isExit) {

if (bytes32(0) == trieKey) {

owner = requestor;

} ...

} else {

if (bytes32(0) == trieKey) {

require(owner == requestor);

} ... else {

// cannot apply request on other variables.

revert();

}

}

...

}

27

28 of 47

function applyRequestInChildChain(...) ... {

if (isExit) {

if (bytes32(0) == trieKey) {

require(requestor == owner);

} ... else { // cannot exit other variables.

revert();

}

} else { // apply enter

if (bytes32(0) == trieKey) {

owner = requestor;

} ...

}

...

}

28

29 of 47

Rule 4. 사전에 정의된 trieKey 가 아니라면 항상 revert 하자.

Enter in RootChain, Exit in ChildChain 에서 revert 한다면

Enter in ChildChain, Exit in RootChain를 apply 하는 일은 없다.

29

30 of 47

function applyRequestInRootChain(...) ... {

require(msg.sender == address(rootchain));

require(trieKey == keccak256(abi.encodePacked(requestor, bytes32(0))));

uint v = decodeTrieValue(trieValue);

if (isExit) {

mint(requestor, v);

} else {

burn(requestor, v);

}

emit RequestCreated(isExit, requestor, trieKey, v);

return true;

}

30

31 of 47

function applyRequestInChildChain(...) ... {

require(development || msg.sender == address(0));

require(trieKey == keccak256(abi.encodePacked(requestor, bytes32(0))));

uint v = decodeTrieValue(trieValue);

if (isExit) {

burn(requestor, v);

} else {

mint(requestor, v);

}

emit RequestCreated(isExit, requestor, trieKey, v);

return true;

}

31

32 of 47

Tip 2. trieKey에 소유권을 명시하고 싶다면 sha3(requestor, variable index)를 사용하자.

32

33 of 47

3. Multisig Wallet: RequestableMultisig

Gnosis 구현체와 다른 점

  • transaction id를 sequential number가 아닌 hash를 이용하여 생성
  • transaction id만 있으면 confirm 할 수 있음 (단 execution 은 tx 데이터 필요)

33

34 of 47

3. Multisig Wallet: RequestableMultisig

function applyRequestInRootChain(...) ... {

require(msg.sender == rootchain);

_handle(true, isExit, requestId, requestor, trieKey, trieValue);

}

function applyRequestInChildChain(...) … {

require(msg.sender == address(0));

_handle(false, isExit, requestId, requestor, trieKey, trieValue);

}

34

35 of 47

3. Multisig Wallet: RequestableMultisig

function _handle(...) internal {

...

if (trieKey == bytes32(0x00)) {

_handleTransaction(isRootChain, isExit, toTransaction(trieValue));

} else if (trieKey == bytes32(0x01)) {

_handleTransactionId(isExit, trieValue.toBytes32());

} else if (trieKey == bytes32(0x02)) {

_handleExecuted(isExit, trieValue.toBytes32());

} else if (trieKey == bytes32(0x03)) {

_handleNewConfirmation(isRootChain, isExit, requestor, trieValue.toBytes32());

} else if (trieKey == bytes32(0x04)) {

_handleRevokedConfirmation(isRootChain, isExit, requestor, trieValue.toBytes32());

} else if (trieKey == bytes32(0x05)) {

_handleNewOwner(isExit, trieValue.toAddress());

} else if (trieKey == bytes32(0x06)) {

_handleRemovedOwner(isExit, trieValue.toAddress());

} else if (trieKey == bytes32(0x07)) {

required = trieValue.toUint();

emit RequirementChange(trieValue.toUint());

}

...

}

https://github.com/Onther-Tech/requestable-multisig/blob/master/contracts/RequestableMultisig.sol#L232

35

36 of 47

3. Multisig Wallet: RequestableMultisig

Tip 3. Request 종류에 따라 가능한 trieKey를 그 갯수만큼 늘리면 된다. 하지만 trieValue에 데이터를 더 추가하여 Request의 종류를 줄일 수 도 있다. (trade-off)

case 1)

Request for new confirmation → trieKey = 0x03, trieValue = transactionId�Request for revoked confirmation → trieKey = 0x04, trieValue = transactionId

case 2)

Request for confirmation → trieKey = 0x03, trieValue = transactionId + � confirmation

36

37 of 47

3. Multisig Wallet: RequestableMultisig

function _handleTransaction(bool isRootChain, bool isExit, Transaction memory transaction) internal {

bytes32 transactionId = hash(transaction);

// transaction check

// isRootChain == true isRootChain == false

// +--------------------------------------------------

// enter request | must be added | must not be added

// exit request | must not be added | must be added

if (isRootChain && !isExit || !isRootChain && isExit) {

require(transactions[transactionId].added);

} else {

require(!transactions[transactionId].added);

addTransaction(transaction.destination, transaction.value, transaction.data);

}

}

37

transaction : trieValue 를 RLP decode 한 구조체 (toTransaction())

Rule 2. Enter in RootChain과 Exit in ChildChain은 동일한 로직.�마찬가지로 Exit in RootChain과 Enter in ChildChain은 동일한 로직.

38 of 47

3. Multisig Wallet: RequestableMultisig

Tip 4. trieValue에 32 바이트보다 긴 데이터를 넣어야 할 경우 RLP 인코딩을 사용하자.

  • Solidity: RLP.sol (only decoding), RLPEncode.sol (only encoding)
  • JavaScript: rlp (encoding, decoding)
  • Python: pyrlp (encoding, decoding)

38

39 of 47

3. Multisig Wallet: RequestableMultisig

function _handleNewConfirmation(bool isRootChain, bool isExit, address requestor, bytes32 transactionId) internal notExecuted(transactionId) {

// confirmation check

// isRootChain == true isRootChain == false

// +--------------------------------------------------

// enter request | must be confirmed | must not be confirmed

// exit request | must not be confirmed | must be confirmed

if (isRootChain && !isExit || !isRootChain && isExit) {

require(confirmations[transactionId][requestor]);

confirmations[transactionId][requestor] = false;

// What evnet should be fired?

} else {

require(!confirmations[transactionId][requestor]);

confirmations[transactionId][requestor] = true;

emit Confirmation(requestor, transactionId);

}

}

39

40 of 47

3. Multisig Wallet: RequestableMultisig

function _handleNewOwner(bool isRootChain, bool isExit, address owner) internal {

if (isRootChain && !isExit || !isRootChain && isExit) {

require(isOwner[owner]);

} else {

this.addOwner(owner);

}

}

function _handleRemovedOwner(bool isRootChain, bool isExit, address owner) internal {

if (isRootChain && !isExit || !isRootChain && isExit) {

require(!isOwner[owner]);

} else {

this.removeOwner(owner);

}

}

40

41 of 47

3. Multisig Wallet: RequestableMultisig

Rule 5. 배열의 원소를 Request를 통해 다른 체인으로 전달할 수 있지만, 원소의 순서는 다를 수 있다. 순서가 중요한 배열의 경우 non-requestable해야 한다.

41

42 of 47

결론

Rule 1. 한 쪽에서 값을 증가시키면 다른쪽에서 그만큼 감소시키자.

Rule 2. Enter in RootChain과 Exit in ChildChain은 동일한 로직. 마찬가지로 Exit in RootChain과 Enter in ChildChain은 동일한 로직.

Rule 3. 값 일부가 아닌 전체를 Request 할 땐 증감(+,-)이 아닌 할당(=) 연산을 사용하자. 또한 증감 연산은 uint, int 자료형에만 사용하자.

Rule 4. 사전에 정의된 trieKey 가 아니라면 항상 revert!

Rule 5. 배열의 원소를 Request를 통해 다른 체인으로 전달할 수 있지만, 원소의 순서는 다를 수 있다. 순서가 중요한 배열의 경우 non-requestable!

Tip 1. 특정 변수에 하나의 연산(e.g. +)만 사용하고 싶다면 Enter in RootChain과 Exit in ChildChain 에서 관여하는 변수와 � Enter in ChildChain과 Exit in RootChain에서 관여하는 변수를 다르게 두자.

Tip 2. trieKey에 소유권을 명시하고 싶다면 sha3(requestor, variable index)를 사용하자.

Tip 3. Request 종류에 따라 가능한 trieKey를 그 갯수만큼 늘리면 된다. 하지만 trieValue에 데이터를 더 추가하여 Request의 종류를 줄일 수 도 있다. (tradeoff)

Tip 4. trieValue에 32 바이트보다 긴 데이터를 넣어야 할 경우 RLP 인코딩을 사용하자.

42

43 of 47

4. Make CryptoKitties Requestable

KittyAccessControll.paused: only enter by anyone

KittyAccessControll.ceoAddress: only enter by anyone

KittyAccessControll.cfoAddress: only enter by anyone

KittyAccessControll.cooAddress: only enter by anyone

KittyBreeding.autoBirthFee: only enter by anyone

43

44 of 47

4. Make CryptoKitties Requestable

KittyBase.kitties: enter or exit by anyone

KittyBase.kittyIndexToOwner: enter and exit by kitty owner

KittyBase.kittyIndexToApproved: non-requestable.

KittyBase.ownershipTokenCount: non-requestable.

KittyBase.sireAllowedToAddress: non-requestable

KittyBreeding.pregnantKitties: Pregenent Kitty Ownership Request enter / exit 시 증감

44

45 of 47

4. Make CryptoKitties Requestable

KittyBase.saleAuction: non-requestable. set by CEO

KittyBase.siringAuction: non-requestable. set by CEO

KittyBreeding.geneScience: non-requestable. set by CEO

KittyCore.newContractAddress: non-requestable. set by CEO

KittyMinting.promoCreatedCount: only enter by anyone

KittyMinting.gen0CreatedCount: only enter by anyone

45

46 of 47

Please Visit

http://tokamak.network

46

47 of 47

출처

47