Techhack for life

テクノロジー関連で学んだことを書いていきます。

Vyper -サンプルより学ぶ-

Vyperの公式ホームページでは、サンプルコードが5つ紹介されています。
本ブログでは、それらからどのようにVyperでコーディングをするのか、学んでいきたいと思います。

紹介するコード

  1. Simple Open Auction
  2. Safe Remote Purchase
  3. Crowdfund
  4. Voting
  5. Company Stock

1. Simple Open Auction

# Open Auction

# Auction params
# Beneficiary receives money from the highest bidder
beneficiary: public(address)
auction_start: public(timestamp)
auction_end: public(timestamp)

# Current state of auction
highest_bidder: public(address)
highest_bid: public(wei_value)

# Set to true at the end, disallows any change
ended: public(bool)

# Create a simple auction with `_bidding_time`
# seconds bidding time on behalf of the
# beneficiary address `_beneficiary`.
@public
def __init__(_beneficiary: address, _bidding_time: timedelta):
    self.beneficiary = _beneficiary
    self.auction_start = block.timestamp
    self.auction_end = self.auction_start + _bidding_time

# Bid on the auction with the value sent
# together with this transaction.
# The value will only be refunded if the
# auction is not won.
@public
@payable
def bid():
    # Check if bidding period is over.
    assert block.timestamp < self.auction_end
    # Check if bid is high enough
    assert msg.value > self.highest_bid
    if not self.highest_bid == 0:
        # Sends money back to the previous highest bidder
        send(self.highest_bidder, self.highest_bid)
    self.highest_bidder = msg.sender
    self.highest_bid = msg.value


# End the auction and send the highest bid
# to the beneficiary.
@public
def end_auction():
    # It is a good guideline to structure functions that interact
    # with other contracts (i.e. they call functions or send Ether)
    # into three phases:
    # 1. checking conditions
    # 2. performing actions (potentially changing conditions)
    # 3. interacting with other contracts
    # If these phases are mixed up, the other contract could call
    # back into the current contract and modify the state or cause
    # effects (Ether payout) to be performed multiple times.
    # If functions called internally include interaction with external
    # contracts, they also have to be considered interaction with
    # external contracts.

    # 1. Conditions
    # Check if auction endtime has been reached
    assert block.timestamp >= self.auction_end
    # Check if this function has already been called
    assert not self.ended

    # 2. Effects
    self.ended = True

    # 3. Interaction
    send(self.beneficiary, self.highest_bid)

変数の設定方法

変数名: アクセス修飾子(型)

beneficiary: public(address)

publicを記述することで、自動的にゲッターが設定される

コンストラクター

initを定義する

@public
def __init__(_beneficiary: address, _bidding_time: timedelta):
    self.beneficiary = _beneficiary
    self.auction_start = block.timestamp
    self.auction_end = self.auction_start + _bidding_time
  • 関数定義の前に "@public" を記述することで、外部からの呼びだしが可能になる
  • 自身のステート変数に値を設定する場合、self.valueble = ...のように、”self”を記述する
  • "timestamp"型の差分は、”timedelta"型を使わないとエラー

その他

@public
@payable
def bid():
    # Check if bidding period is over.
    assert block.timestamp < self.auction_end
    # Check if bid is high enough
    assert msg.value > self.highest_bid
    if not self.highest_bid == 0:
        # Sends money back to the previous highest bidder
        send(self.highest_bidder, self.highest_bid)
    self.highest_bidder = msg.sender
    self.highest_bid = msg.value
  • 関数定義の前に "@payable" を記述することで、コントラクトに関数を通してetherを受け取ることが可能になる
  • assertで、状態のチェックをすることができる。Trueの場合、次の行に進める
  • assert notで、Falseの場合、次の行に進める
  • send(address, wei_value)で、etherをaddressに送信できる

2. Safe Remote Purchase

#Safe Remote Purchase
#Originally from
#https://github.com/ethereum/solidity/blob/develop/docs/solidity-by-example.rst
#ported to vyper and optimized

#Rundown of the transaction:
#1. Seller posts item for sale and posts safety deposit of double the item value.
# Balance is 2*value.
#(1.1. Seller can reclaim deposit and close the sale as long as nothing was purchased.)
#2. Buyer purchases item (value) plus posts an additional safety deposit (Item value).
# Balance is 4*value.
#3. Seller ships item.
#4. Buyer confirms receiving the item. Buyer's deposit (value) is returned.
#Seller's deposit (2*value) + items value is returned. Balance is 0.

value: public(wei_value) #Value of the item
seller: public(address)
buyer: public(address)
unlocked: public(bool)
#@constant
#def unlocked() -> bool: #Is a refund possible for the seller?
#    return (self.balance == self.value*2)

@public
@payable
def __init__():
    assert (msg.value % 2) == 0
    self.value = msg.value / 2  #The seller initializes the contract by
        #posting a safety deposit of 2*value of the item up for sale.
    self.seller = msg.sender
    self.unlocked = True

@public
def abort():
    assert self.unlocked #Is the contract still refundable?
    assert msg.sender == self.seller #Only the seller can refund
        # his deposit before any buyer purchases the item.
    selfdestruct(self.seller) #Refunds the seller and deletes the contract.

@public
@payable
def purchase():
    assert self.unlocked #Is the contract still open (is the item still up for sale)?
    assert msg.value == (2 * self.value) #Is the deposit the correct value?
    self.buyer = msg.sender
    self.unlocked = False

@public
def received():
    assert not self.unlocked #Is the item already purchased and pending confirmation
        # from the buyer?
    assert msg.sender == self.buyer
    send(self.buyer, self.value) #Return the buyer's deposit (=value) to the buyer.
    selfdestruct(self.seller) #Return the seller's deposit (=2*value)
        # and the purchase price (=value) to the seller.s

コントラクトからetherの送信と、コントラクトの破壊

  • selfdestruct(address)により、コントラクトを破棄して、このコントラクトが所有しているEtherを引数のアドレスに送信する

3. Crowdfund

# Setup private variables (only callable from within the contract)
funders: {sender: address, value: wei_value}[int128]
nextFunderIndex: int128
beneficiary: address
deadline: timestamp
goal: wei_value
refundIndex: int128
timelimit: timedelta


# Setup global variables
@public
def __init__(_beneficiary: address, _goal: wei_value, _timelimit: timedelta):
    self.beneficiary = _beneficiary
    self.deadline = block.timestamp + _timelimit
    self.timelimit = _timelimit
    self.goal = _goal


# Participate in this crowdfunding campaign
@public
@payable
def participate():
    assert block.timestamp < self.deadline

    nfi: int128 = self.nextFunderIndex

    self.funders[nfi] = {sender: msg.sender, value: msg.value}
    self.nextFunderIndex = nfi + 1


# Enough money was raised! Send funds to the beneficiary
@public
def finalize():
    assert block.timestamp >= self.deadline and self.balance >= self.goal

    selfdestruct(self.beneficiary)


# Not enough money was raised! Refund everyone (max 30 people at a time
# to avoid gas limit issues)
@public
def refund():
    assert block.timestamp >= self.deadline and self.balance < self.goal

    ind: int128 = self.refundIndex

    for i in range(ind, ind + 30):
        if i >= self.nextFunderIndex:
            self.refundIndex = self.nextFunderIndex
            return

        send(self.funders[i].sender, self.funders[i].value)
        self.funders[i] = None

    self.refundIndex = ind + 30

構造体・配列

構造体の配列を1行で定義することができる

funders: {sender: address, value: wei_value}[int128]

配列へのアクセス

rangeを用いる

for i in range(ind, ind + 30):
    if i >= self.nextFunderIndex:
        self.refundIndex = self.nextFunderIndex
        return

    send(self.funders[i].sender, self.funders[i].value)
    self.funders[i] = None

self.refundIndex = ind + 30
  • for index in range(from, to) fromから、to番までの配列の中身を順次取り出す
  • コントラクトに紐づけられているether量は、self.balanceでチェックする

4.Voting

# Voting with delegation.

# Information about voters
voters: public({
    # weight is accumulated by delegation
    weight: int128,
    # if true, that person already voted (which includes voting by delegating)
    voted: bool,
    # person delegated to
    delegate: address,
    # index of the voted proposal, which is not meaningful unless `voted` is True.
    vote: int128
}[address])

# This is a type for a list of proposals.
proposals: public({
    # short name (up to 32 bytes)
    name: bytes32,
    # int128ber of accumulated votes
    vote_count: int128
}[int128])

voter_count: public(int128)
chairperson: public(address)
int128_proposals: public(int128)

@public
@constant
def delegated(addr: address) -> bool:
    # equivalent to
        # self.voters[addr].delegate !=  0x0000000000000000000000000000000000000000
    return not not self.voters[addr].delegate

@public
@constant
def directly_voted(addr: address) -> bool:
    # not <address> equivalent to
        # 
 ==  0x0000000000000000000000000000000000000000

    return self.voters[addr].voted and not self.voters[addr].delegate

# Setup global variables
@public
def __init__(_proposalNames: bytes32[2]):
    self.chairperson = msg.sender
    self.voter_count = 0
    for i in range(2):
        self.proposals[i] = {
            name: _proposalNames[i],
            vote_count: 0
        }
        self.int128_proposals += 1

# Give a `voter` the right to vote on this ballot.
# This may only be called by the `chairperson`.
@public
def give_right_to_vote(voter: address):
    # Throws if the sender is not the chairperson.
    assert msg.sender == self.chairperson
    # Throws if the voter has already voted.
    assert not self.voters[voter].voted
    # Throws if the voter's voting weight isn't 0.
    assert self.voters[voter].weight == 0
    self.voters[voter].weight = 1
    self.voter_count += 1

# Used by `delegate` below, and can be called by anyone.
@public
def forward_weight(delegate_with_weight_to_forward: address):
    assert self.delegated(delegate_with_weight_to_forward)
    # Throw if there is nothing to do:
    assert self.voters[delegate_with_weight_to_forward].weight > 0

    target: address = self.voters[delegate_with_weight_to_forward].delegate
    for i in range(4):
        if self.delegated(target):
            target = self.voters[target].delegate
            # The following effectively detects cycles of length <= 5,
            # in which the delegation is given back to the delegator.
            # This could be done for any int128ber of loops,
            # or even infinitely with a while loop.
            # However, cycles aren't actually problematic for correctness;
            # they just result in spoiled votes.
            # So, in the production version, this should instead be
            # the responsibility of the contract's client, and this
            # check should be removed.
            assert target != delegate_with_weight_to_forward
        else:
            # Weight will be moved to someone who directly voted or
            # hasn't voted.
            break

    weight_to_forward: int128 = self.voters[delegate_with_weight_to_forward].weight
    self.voters[delegate_with_weight_to_forward].weight = 0
    self.voters[target].weight += weight_to_forward

    if self.directly_voted(target):
        self.proposals[self.voters[target].vote].vote_count += weight_to_forward
        self.voters[target].weight = 0

    # To reiterate: if target is also a delegate, this function will need
    # to be called again, similarly to as above.

# Delegate your vote to the voter `to`.
@public
def delegate(to: address):
    # Throws if the sender has already voted
    assert not self.voters[msg.sender].voted
    # Throws if the sender tries to delegate their vote to themselves or to
    # the default address value of  0x0000000000000000000000000000000000000000
    # (the latter might not be problematic, but I don't want to think about it).
    assert to != msg.sender and not not to

    self.voters[msg.sender].voted = True
    self.voters[msg.sender].delegate = to

    # This call will throw if and only if this delegation would cause a loop
        # of length <= 5 that ends up delegating back to the delegator.
    self.forward_weight(msg.sender)

# Give your vote (including votes delegated to you)
# to proposal `proposals[proposal].name`.
@public
def vote(proposal: int128):
    # can't vote twice
    assert not self.voters[msg.sender].voted
    # can only vote on legitimate proposals
    assert proposal < self.int128_proposals

    self.voters[msg.sender].vote = proposal
    self.voters[msg.sender].voted = True

    # transfer msg.sender's weight to proposal
    self.proposals[proposal].vote_count += self.voters[msg.sender].weight
    self.voters[msg.sender].weight = 0

# Computes the winning proposal taking all
# previous votes into account.
@public
@constant
def winning_proposal() -> int128:
    winning_vote_count: int128 = 0
    winning_proposal: int128 = 0
    for i in range(2):
        if self.proposals[i].vote_count > winning_vote_count:
            winning_vote_count = self.proposals[i].vote_count
            winning_proposal = i
    return winning_proposal

# Calls winning_proposal() function to get the index
# of the winner contained in the proposals array and then
# returns the name of the winner
@public
@constant
def winner_name() -> bytes32:
    return self.proposals[self.winning_proposal()].name

状態を変更しない

@public
@constant
def winning_proposal() -> int128:
    winning_vote_count: int128 = 0
    winning_proposal: int128 = 0
    for i in range(2):
        if self.proposals[i].vote_count > winning_vote_count:
            winning_vote_count = self.proposals[i].vote_count
            winning_proposal = i
    return winning_proposal
  • @constantで、状態を変更しないことを明記する
  • "-> 型"で、返り値を設定する。returnで値を返す

5.Company Stock

units: {
    currency_value: "Currency Value"
}

# Financial events the contract logs
Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256(currency_value)})
Buy: event({_buyer: indexed(address), _buy_order: uint256(currency_value)})
Sell: event({_seller: indexed(address), _sell_order: uint256(currency_value)})
Pay: event({_vendor: indexed(address), _amount: wei_value})

# Initiate the variables for the company and it's own shares.
company: public(address)
total_shares: public(uint256(currency_value))
price: public(uint256 (wei / currency_value))

# Store a ledger of stockholder holdings.
holdings: uint256(currency_value)[address]

# Set up the company.
@public
def __init__(_company: address, _total_shares: uint256(currency_value),
        initial_price: uint256(wei / currency_value) ):
    assert _total_shares > 0
    assert initial_price > 0

    self.company = _company
    self.total_shares = _total_shares
    self.price = initial_price

    # The company holds all the shares at first, but can sell them all.
    self.holdings[self.company] = _total_shares

@public
@constant
def stock_available() -> uint256(currency_value):
    return self.holdings[self.company]

# Give some value to the company and get stock in return.
@public
@payable
def buy_stock():
    # Note: full amount is given to company (no fractional shares),
    #       so be sure to send exact amount to buy shares
    buy_order: uint256(currency_value) = msg.value / self.price # rounds down

    # Check that there are enough shares to buy.
    assert self.stock_available() >= buy_order

    # Take the shares off the market and give them to the stockholder.
    self.holdings[self.company] -= buy_order
    self.holdings[msg.sender] += buy_order

    # Log the buy event.
    log.Buy(msg.sender, buy_order)

# Find out how much stock any address (that's owned by someone) has.
@public
@constant
def get_holding(_stockholder: address) -> uint256(currency_value):
    return self.holdings[_stockholder]

# Return the amount the company has on hand in cash.
@public
@constant
def cash() -> wei_value:
    return self.balance

# Give stock back to the company and get money back as ETH.
@public
def sell_stock(sell_order: uint256(currency_value)):
    assert sell_order > 0 # Otherwise, this would fail at send() below,
        # due to an OOG error (there would be zero value available for gas).
    # You can only sell as much stock as you own.
    assert self.get_holding(msg.sender) >= sell_order
    # Check that the company can pay you.
    assert self.cash() >= (sell_order * self.price)

    # Sell the stock, send the proceeds to the user
    # and put the stock back on the market.
    self.holdings[msg.sender] -= sell_order
    self.holdings[self.company] += sell_order
    send(msg.sender, sell_order * self.price)

    # Log the sell event.
    log.Sell(msg.sender, sell_order)

# Transfer stock from one stockholder to another. (Assume that the
# receiver is given some compensation, but this is not enforced.)
@public
def transfer_stock(receiver: address, transfer_order: uint256(currency_value)):
    assert transfer_order > 0 # This is similar to sell_stock above.
    # Similarly, you can only trade as much stock as you own.
    assert self.get_holding(msg.sender) >= transfer_order

    # Debit the sender's stock and add to the receiver's address.
    self.holdings[msg.sender] -= transfer_order
    self.holdings[receiver] += transfer_order

    # Log the transfer event.
    log.Transfer(msg.sender, receiver, transfer_order)

# Allow the company to pay someone for services rendered.
@public
def pay_bill(vendor: address, amount: wei_value):
    # Only the company can pay people.
    assert msg.sender == self.company
    # Also, it can pay only if there's enough to pay them with.
    assert self.cash() >= amount

    # Pay the bill!
    send(vendor, amount)

    # Log the payment event.
    log.Pay(vendor, amount)

# Return the amount in wei that a company has raised in stock offerings.
@public
@constant
def debt() -> wei_value:
    return (self.total_shares - self.holdings[self.company]) * self.price

# Return the cash holdings minus the debt of the company.
# The share debt or liability only is included here,
# but of course all other liabilities can be included.
@public
@constant
def worth() -> wei_value:
    return self.cash() - self.debt()
イベント
  • eventは、以下のように記述する
    • Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256(currency_value)})
    • 呼び出す時は、log." イベント名"を呼び出す。log.Transfer(msg.sender, receiver, transfer_order)
単位
  • unitsを用いることで、単位を設定できる
units: {
    currency_value: "Currency Value"
}
  • 設定した単位の使用方法は以下のようにする
total_shares: public(uint256(currency_value))

まとめ

サンプルから、標準的な実装方法を学ぶことが可能ですね。 ただ、ビルトイン関数は他に多くあり、これらからは学ぶことができませんでした。 次回のブログでは、その関数を確認してみたいと思います。

参照

Vyper by Example — Vyper documentation