Skip to content

Red Paper ++ sruby - Small, Smart, Secure, Safe, Solid & Sound (S6) Ruby - The Ruby Programming Language for Contract / Transaction Scripts on the Blockchain World Computer - Yes, It's Just Ruby

License

s6ruby/redpaper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 

Repository files navigation

The Ruby Programming Language for Contract / Transaction Scripts on the Blockchain World Computer - Yes, It's Just Ruby

sruby - Small, Smart, Secure, Safe, Solid & Sound (S6) Ruby

sruby is a subset of mruby [1] that is a subset of "classic" ruby [2].

What's missing and why?

Less is more. The golden rule of secure code is keep it simple, stupid.

  • NO inheritance
  • NO recursion
  • NO re-entrance - auto-magic protection on function calls
  • NO floating point numbers or arithmetic
  • NO overflow & underflow in numbers - auto-magic "safe-math" protection
  • NO null (nil) - all variables, structs and hash mappings have default (zero) values
  • and much much more

What's the upside?

You can cross-compile (transpile) contract scripts (*) to:

  • Solidity [3] - JavaScript-like contract scripts
  • Vyper [4] - Python-like contract scripts
  • Yul [5] - EVM (Ethereum Virtual Machine) Assembly-like intermediate contract scripts
  • Liquidity [6] - OCaml-like (or ReasonML-like) contract scripts
  • and much much more

(*) in the future.

Yes, yes, yes - It's just "plain-vanilla" ruby

Remember - the code is and always will be just "plain-vanilla" ruby that runs with "classic" ruby or mruby "out-of-the-box".

NEW IN 2023

Let's update the syntax / style for compatibility with the big brother / sister - Rubidity!

New to Rubidity? See Rubidity - Ruby for Layer 1 (L1) Contracts / Protocols with "Off-Chain" Indexer »

Bonus - Yes, you can. It's just Ruby. Run the sample contracts from the Red Paper with Rubidity and Simulacrum »

Contract Samples

Hello, World! - Greeter

############################
# Greeter Contract 

storage owner:    Address,
        greeting: String

# @sig (String)
def setup( greeting: )
  @owner    = msg.sender
  @greeting = greeting
end

# @sig () => String
def greet
  @greeting
end

# @sig ()
def kill
  selfdestruct( msg.sender )  if msg.sender == @owner
end

Mint Your Own Money - Minimal Viable Token

#######################
# Token Contract

storage balance_of:  Mapping( Address, UInt )
                     
# @sig (UInt)
def setup( initial_supply: )
  @balance_of[ msg.sender] = initial_supply
end

# @sig (Address, UInt) => Bool
def transfer( to:, value: )
  assert @balance_of[ msg.sender ] >= value, 'insufficient funds'
  assert @balance_of[ to ] + value >= @balance_of[ to ], 'overflow - transfer value too big'

  @balance_of[ msg.sender ] -= value
  @balance_of[ to ]         += value

  true
end

Win x65 000 - Roll the (Satoshi) Dice

################################
# Satoshi Dice Contract

event :BetPlaced, id: UInt, user: Address, cap: UInt, amount: UInt
event :Roll,      id: UInt, rolled: UInt

struct :Bet,
         user:   Address, 
         block:  UInt,  
         cap:    UInt, 
         amount: UInt  

storage   owner:    Address,
          counter:  UInt,
          bets:     Mapping( UInt, Bet ) 

## Fee (Casino House Edge) is 1.9%, that is, 19 / 1000
FEE_NUMERATOR   = 19
FEE_DENOMINATOR = 1000

MAXIMUM_CAP = 2**16   # 65_536 = 2^16 = 2 byte/16 bit
MAXIMUM_BET = 100_000_000
MINIMUM_BET = 100
                      
# @sig ()
def setup
  @owner  = msg.sender
end

# @sig (UInt) 
def bet( cap: )
  assert cap >= 1 && cap <= MAXIMUM_CAP
  assert msg.value >= MINIMUM_BET && msg.value <= MAXIMUM_BET

  @counter += 1
  @bets[@counter] = Bet.new( msg.sender, block.number+3, cap, msg.value )
  
  log BetPlaced, id: @counter, 
                 user: msg.sender, 
                 cap: cap, 
                 amount: msg.value
end

# @sig (UInt)
def roll( id: )
  bet = @bets[id]

  assert msg.sender == bet.user,  'only better can roll'
  assert block.number >= bet.block, 'block before bet block; cannot roll'
  assert block.number <= bet.block + 255,  'block beyond 255 blocks; cannot roll'

  ## "provable" fair - random number depends on
  ##  - blockhash (of block in the future - t+3)
  ##  - nonce (that is, bet counter id)
  hex = sha256( "#{blockhash( bet.block )} #{id}" )
  ## get first 2 bytes (4 chars in hex string) and convert to integer number
  ##   results in a number between 0 and 65_535
  rolled = hex_to_i( hex[0,4] )

  if rolled < bet.cap
     payout = bet.amount * MAXIMUM_CAP / bet.cap
     fee = payout * FEE_NUMERATOR / FEE_DENOMINATOR
     payout -= fee

     msg.sender.transfer( payout )
  end

  log Roll, id: id, rolled: rolled
  @bets.delete( id )
end

# @sig ()
def fund
end

# @sig ()
def kill
  assert msg.sender == @owner
  selfdestruct( @owner )
end

Kick Start Your Project with a Crowd Funder

##############################
# Crowd Funder Contract

event :FundingReceived, address: Address, 
                        amount: UInt, 
                        current_total: UInt
event :WinnerPaid, winner_address: Address


enum :State, :fundraising, :expired_refund, :successful

struct :Contribution,
         amount:      UInt, 
         contributor: Address

storage   creator:          Address,
          fund_recipient:   Address,
          campaign_url:     String,
          minimum_to_raise: UInt,
          raise_by:         Timestamp,
          state:            State,
          total_raised:     UInt,
          complete_at:      Timestamp,
          contributions:    Array( Contribution ) 

# @sig (Timedelta, String, Address, UInt)
def setup(
      time_in_hours_for_fundraising:,
      campaign_url:,
      fund_recipient:,
      minimum_to_raise: )

  @creator          = msg.sender
  @fund_recipient   = fund_recipient   # note: creator may be different than recipient
  @campaign_url     = campaign_url
  @minimum_to_raise = minimum_to_raise # required to tip, else everyone gets refund
  @raise_by         = block.timestamp + (time_in_hours_for_fundraising * 1.hour )
  @state            = State.fundraising
end


# @sig ()
def pay_out
  assert @state.successful?,  'state must be set to successful for pay out'

  @fund_recipient.transfer( this.balance )
  log WinnerPaid, winner_address: @fund_recipient
end

# @sig ()
def check_if_funding_complete_or_expired
  if @total_raised > @minimum_to_raise
    @state = State.successful
    pay_out()
  elsif block.timestamp > @raise_by
    # note: backers can now collect refunds by calling refund(id)
    @state = State.expired_refund
    @complete_at = block.timestamp
  end
end

# @sig ()
def contribute
  assert @state.fundraising?, 'state must be set to fundraising to contribute'

  @contributions.push( Contribution.new( msg.value, msg.sender ))
  @total_raised += msg.value

  log FundingReceived, address: msg.sender, 
                       amount: msg.value,
                       current_total: @total_raised 

  check_if_funding_complete_or_expired()

  @contributions.size - 1   # return (contribution) id
end


# @sig (UInt)
def refund( id: )
  assert @state.expired_refund?, 'state must be set to expired_refund to refund'
  assert @contributions.size > id && id >= 0 && @contributions[id].amount != 0,  'contribution id out-of-range'

  amount_to_refund = @contributions[id].amount
  @contributions[id].amount = 0

  @contributions[id].contributor.transfer( amount_to_refund )

  true
end

# @sig
def kill
  assert msg.sender == @creator,  'only creator can kill contract'
  # wait 24 weeks after final contract state before allowing contract destruction
  assert @state.expired_refund? || @state.successful?, 'state must be set to expired_refund'  
  aasert @complete_at + 24.weeks < block.timestamp,  'complete_at time must be beyond 24 weeks'

  # note: creator gets all money that hasn't be claimed
  selfdestruct( msg.sender )
end

Liquid / Delegative Democracy - Let's Vote (or Delegate Your Vote) on a Proposal

#########################
# Ballot Contract

struct :Voter,
          weight:   UInt,     # weight is accumulated by delegation
          voted:    Bool,     # if true, that person already voted
          vote:     UInt,     # index of the voted proposal
          delegate: Address   # person delegated to

struct :Proposal, 
          vote_count: UInt    # number of accumulated votes

storage  chairperson: Address,
         voters:      Mapping( Address, Voter ), 
         proposals:   Array( Proposal )    

## Create a new ballot with $(num_proposals) different proposals.
##   @sig [UInt]
def constructor( num_proposals: )
  @chairperson = msg.sender
  @proposals.length = num_proposals
  
  @voters[ @chairperson ].weight = 1
end

## Give $(to_voter) the right to vote on this ballot.
## May only be called by $(chairperson).
##  @sig [Address]
def give_right_to_vote( to_voter: ) 
   assert msg.sender == @chairperson, "only chairperson"
   aasert @voters[to_voter].voted? == false, "voter already voted"
     
   @voters[to_voter].weight = 1
end

## Delegate your vote to the voter $(to).
##   @sig [Address]
def delegate( to: )
  sender = @voters[msg.sender]  # assigns reference
  assert sender.voted? == false

  while @voters[to].delegate != address(0) && @voters[to].delegate != msg.sender do
    to = @voters[to].delegate
  end
  assert to != msg.sender

  sender.voted    = true
  sender.delegate = to
  delegate_to = @voters[to]
  if delegate_to.voted
    @proposals[delegate_to.vote].vote_count += sender.weight
  else
    delegate_to.weight += sender.weight
  end
end

## Give a single vote to proposal $(to_proposal).
##  @sig [UInt]
def vote( to_proposal: )
  sender = @voters[msg.sender]
  assert sender.voted? == false && to_proposal < @proposals.length
  sender.voted = true
  sender.vote  = to_proposal
  @proposals[to_proposal].vote_count += sender.weight
end

##  @sig [], :view, returns: UInt
def winning_proposal
  winning_vote_count = 0 
  winning_proposal   = 0
  @proposals.each_with_index do |proposal,i|
    if proposal.vote_count > winning_vote_count
      winning_vote_count = proposal.vote_count
      winning_proposal   = i
    end
  end
  winning_proposal
end

Types (Work-In-Progress)

Value TypesReference Types

Value Types

Bool • Integer (Money • Timestamp • Timedelta • Enum) • Address • String • Byte Array / Bytes

Bool

Class: Bool

Values: true | false

Zero: false

Bool.zero   #=> false

Integer

Class: Int

Zero: 0

Int.zero   #=> false

Integer Types

Money • Timestamp • Timedelta • Enum

Money (Integer)

Money Units

Timestamp (Integer)

Time Units

Timestamp.now   #=> 1551122309

Timedelta (Integer)

Enum (Integer)

Address

Class: Address

Zero: 0x0000 or 0x0 or Address(0)

Example:

owner = '0x0000'  # or
owner = address(0)

String

Byte Array / Bytes

Reference Types

ArrayStructMapping

Array

Example:

Array()

Struct

Mapping

Event Logging

...

Storage

The contract's state gets stored in contract storage.

...

Request for Comments (RFC)

Send your questions and comments to the ruby-talk mailing list. Thanks!

References

  1. mruby programming language, see https://mruby.org
  2. ruby programming language, see https://www.ruby-lang.org
  3. solidity programming language, see https://solidity.readthedocs.io
  4. vyper programming language, see https://vyper.readthedocs.io
  5. yul intermediate language for the EVM (ethereum virtual machine) assembly code, see the Yul section in the solidity programming language reference
  6. liquidity programing language, see http://www.liquidity-lang.org

About

Red Paper ++ sruby - Small, Smart, Secure, Safe, Solid & Sound (S6) Ruby - The Ruby Programming Language for Contract / Transaction Scripts on the Blockchain World Computer - Yes, It's Just Ruby

Topics

Resources

License

Stars

Watchers

Forks