Inspired by Learn X In Y Minutes - Where X=Solidity (Source) - a reworked edition / version for Rubysol et al.
First, a simple Bank contract. Allows deposits, withdrawals, and balance checks
simple_bank.rb (note .rb extension)
# Start with Natspec comment
# used for documentation - and as descriptive data for UI elements/actions
# @title SimpleBank
# @author geraldb
# 'contract' has similarities to 'class' in other languages
# (class variables, inheritance, etc.)
class SimpleBank < Contract # CapWords
# Declare state (storage) variables outside function, persist through life of contract
# dictionary that maps addresses to balances
# always be careful about overflow attacks with numbers
storage _balance: mapping( Address, UInt )
# 'private' (by using (leading) underscore naming convention)
# means that other contracts
# can't directly query balances
# but data is still viewable to other parties on blockchain
storage owner: Address
# 'public' (by default) makes externally
# readable (not writeable) by users or contracts
# Events - publicize actions to external listeners
event :LogDepositMade accountAddress: Address,
amount: UInt
# Constructor, can receive one or many variables here; only one allowed
sig []
def constructor
# msg provides details about the message that's sent to the contract
# msg.sender is contract caller (address of contract creator)
@owner = msg.sender
end
# @notice Deposit ether into bank
# @return The balance of the user after the deposit is made
sig [], :payable, returns: UInt
def deposit
# Use 'assert' to test user inputs, 'assert' for internal invariants
# Here we are making sure that there isn't an overflow issue
assert @balances[msg.sender] + msg.value >= @balances[msg.sender]
@balances[msg.sender] += msg.value
# no "this." or "self." required with state variable (use @)
# all values set to data type's initial (zero) value by default
log LogDepositMade, msg.sender, msg.value # fire event
@balances[msg.sender]
end
# @notice Withdraw ether from bank
# @dev This does not return any excess ether sent to it
# @param withdrawAmount amount you want to withdraw
# @return remainingBal
sig [UInt], returns: [UInt]
def withdraw( withdrawAmount: )
assert withdrawAmount <= @balances[msg.sender]
# Note the way we deduct the balance right away, before sending
# Every .transfer/.send from this contract can call an external function
# This may allow the caller to request an amount greater
# than their balance using a recursive call
# Aim to commit state before calling external functions, including .transfer/.send
@balances[msg.sender] -= withdrawAmount
# this automatically throws on a failure, which means the updated balance is reverted
msg.sender.transfer(withdrawAmount)
@balances[msg.sender]
end
# @notice Get balance
# @return The balance of the user
#'view' (ex: constant) prevents function from editing state variables;
# allows function to run locally/off blockchain
sig [], :view, returns: UInt
def balance
@balances[msg.sender]
end
end
To be continued ...
A crowdfunding example (broadly similar to Kickstarter).
crowdfunder.rb (note .rb extension)
# @title CrowdFunder
# @author geraldb
class CrowdFunder < Contract
# Variables set on create by creator
storage creator: Address,
fundRecipient: Address, # payable - creator may be different than recipient, and must be payable
minimumToRaise: UInt, # required to tip, else everyone gets refund
campaignUrl: String
# Data structures
enum :State, :fundraising,
:expiredRefund,
:successful
struct :Contribution, amount: UInt,
contributor: Address # payable
# State variables
storage state: State, # initialize on create
totalRaised: UInt,
raiseBy: Timestamp,
completeAt: Timestamp,
contributions: array(Contribution)
event :LogFundingReceived, addr: Address,
amount: UInt,
currentTotal: UInt
event :LogWinnerPaid, winnerAddress: Address
sig [UInt, String, Address, UInt]
def crowdFund(
timeInHoursForFundraising:,
campaignUrl:,
fundRecipient:, # payable
minimumToRaise: )
@creator = msg.sender
@fundRecipient = fundRecipient
@campaignUrl = campaignUrl
@minimumToRaise = minimumToRaise
@raiseBy = now + (timeInHoursForFundraising * 1.hours)
end
sig [], :payable, returns: [UInt]
def contribute
assert @state == State.fundraising
@contributions.push(
Contribution.new(
amount: msg.value,
contributor: msg.sender
) # use array, so can iterate
)
@totalRaised += msg.value
log LogFundingReceived, msg.sender, msg.value, totalRaised
checkIfFundingCompleteOrExpired()
@contributions.length - 1 # return id
end
sig []
def checkIfFundingCompleteOrExpired
if @totalRaised > @minimumToRaise
state = State.successful
payOut()
# could incentivize sender who initiated state change here
elsif now > @raiseBy
state = State.expiredRefund # backers can now collect refunds by calling getRefund(id)
end
@completeAt = now
end
sig []
def payOut
assert @state == State.successful
@fundRecipient.transfer( address(this).balance )
log LogWinnerPaid, fundRecipient
end
sig [UInt], returns: Bool
def getRefund( id: )
assert @state == State.expiredRefund
assert @contributions.length > id && id >= 0
assert @contributions[id].amount != 0
amountToRefund = @contributions[id].amount
@contributions[id].amount = 0
@contributions[id].contributor.transfer(amountToRefund)
true
end
sig []
def removeContract
assert msg.sender == @creator
# Wait 24 weeks after final contract state before allowing contract destruction
assert state == State.expiredRefund || state == State.successful
assert @completeAt + 24.weeks < now
selfdestruct(msg.sender)
# creator gets all money that hasn't be claimed
end
end
Join us in the Rubidity & Rubysol (community) discord (chat server). Yes you can. Your questions and commentary welcome.
Or post them over at the Help & Support page. Thanks.