Examples and Best Practices for Requestable Contracts�(The Hitchhiker’s Guide to Requestable Contracts)
박주형(Carl)
carl.p@onther.io �2019. 5. 27
<외부자료 활용 시 출처 명시 부탁드립니다.>
목차
2
3
1. Requestable Contracts (Enter)
4
5
2. applyRequestIn(Root|Child)Chain function
6
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
2. applyRequestIn(Root|Child)Chain
applyRequestInRootChain
applyRequestInChildChain
8
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
2. applyRequestIn(Root|Child)Chain
bytes32 trieKey
bytes trieValue
10
3. Examples and Best Practices
11
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 를 상속하여 다른 카운터 구현
3. Counter: SimpleCounter
13
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
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
3. Counter: SimpleCounter
Rule 1. 한 체인에서 값을 증가시키면 다른 체인에서 그만큼 감소시키자.
Rule 2. Enter in RootChain과 Exit in ChildChain은 동일한 로직.�마찬가지로 Exit in RootChain과 Enter in ChildChain은 동일한 로직.
구현체 단점: 값이 감소하는 카운터가 감소하는 구현이 맞는걸까?
16
3. Counter: FreezableCounter
17
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
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
3. Counter: FreezableCounter
Rule 3. 값 일부가 아닌 전체를 Request 할 땐 증감(+,-)이 아닌 할당(=) 연산을 사용하자. 또한 증감 연산은 uint, int 자료형에만 사용하자.
구현체 단점: 도중에 두 컨트랙 모두 사용하지 못하는 경우가 올바른가?
20
3. Counter: FreezableCounter
21
3. Counter: TrackableCounter
/// @dev override BaseCounter.count function.
function count() external {
requestableN++;
n++;
emit Counted(n);
}
22
3. Counter: TrackableCounter
Simple Counter
23
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
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
3. Counter: TrackableCounter
Tip 1. 특정 변수에 하나의 연산(e.g. +)만 사용하고 싶다면�Enter in RootChain과 Exit in ChildChain 에서 관여하는 변수와 �Enter in ChildChain과 Exit in RootChain에서 관여하는 변수를 다르게 두자.�
26
3. Token: RequestableSimpleToken
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
3. Token: RequestableSimpleToken
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
3. Token: RequestableSimpleToken
Rule 4. 사전에 정의된 trieKey 가 아니라면 항상 revert 하자.
Enter in RootChain, Exit in ChildChain 에서 revert 한다면
Enter in ChildChain, Exit in RootChain를 apply 하는 일은 없다.
29
3. Token: RequestableERC20WrapperToken
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
3. Token: RequestableERC20WrapperToken
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
3. Token: RequestableERC20WrapperToken
Tip 2. trieKey에 소유권을 명시하고 싶다면 sha3(requestor, variable index)를 사용하자.
32
3. Multisig Wallet: RequestableMultisig
Gnosis 구현체와 다른 점
33
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
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());
}
...
}
35
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
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은 동일한 로직.
3. Multisig Wallet: RequestableMultisig
Tip 4. trieValue에 32 바이트보다 긴 데이터를 넣어야 할 경우 RLP 인코딩을 사용하자.
38
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
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
3. Multisig Wallet: RequestableMultisig
Rule 5. 배열의 원소를 Request를 통해 다른 체인으로 전달할 수 있지만, 원소의 순서는 다를 수 있다. 순서가 중요한 배열의 경우 non-requestable해야 한다.
41
결론
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
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
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
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
Please Visit
http://tokamak.network
46
출처
https://github.com/Onther-Tech/requestable-contract-examples/tree/solidity-counter/counter/solidity
https://github.com/Onther-Tech/plasma-evm-contracts/blob/master/contracts/RequestableSimpleToken.sol
https://github.com/Onther-Tech/requestable-multisig/blob/master/contracts/RequestableMultisig.sol
https://medium.com/onther-tech/cryptokitties-in-plasma-574159c581dc
47