The Ruby Programming Language for Contract / Transaction Scripts on the Blockchain World Computer - Yes, It's Just Ruby
sruby is a subset of mruby [1] that is a subset of "classic" ruby [2].
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
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.
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 »
############################ # 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####################### # 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################################ # 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############################## # 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######################### # 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 endBool • Integer (Money • Timestamp • Timedelta • Enum) • Address • String • Byte Array / Bytes
Class: Bool
Values: true | false
Zero: false
Bool.zero #=> falseClass: Int
Zero: 0
Int.zero #=> falseInteger Types
Money • Timestamp • Timedelta • Enum
Money Units
Time Units
Timestamp.now #=> 1551122309Class: Address
Zero: 0x0000 or 0x0 or Address(0)
Example:
owner = '0x0000' # or owner = address(0)Example:
Array()...
The contract's state gets stored in contract storage.
...
Send your questions and comments to the ruby-talk mailing list. Thanks!
- mruby programming language, see https://mruby.org
- ruby programming language, see https://www.ruby-lang.org
- solidity programming language, see https://solidity.readthedocs.io
- vyper programming language, see https://vyper.readthedocs.io
- yul intermediate language for the EVM (ethereum virtual machine) assembly code, see the Yul section in the solidity programming language reference
- liquidity programing language, see http://www.liquidity-lang.org