In this codelab, you will practice developing and deploying smart contracts of your own. Before starting, you will need an understanding of how to use Ethereum wallets and Remix work from previous codelabs. You may want to more deeply familiarize yourself with the types in Solidity. For this, I would recommend visiting the reference site at http://solidity.readthedocs.io/en/latest/types.html or viewing the associated screencast lectures available from class.
For our first smart contract, we will be creating a fundraiser platform similar to kickstarter, where the owner of the fundraiser sets a fundraising goal that must be met within a certain amount of time. If the fundraiser gets fully funded to a goal, the owner receives the funds, but if it does not reach the goal, then the contributors will be refunded their contributions.
We will need to store a few things in state variables: the address of the owner of the fundraiser, the target goal for amount of funds to be raised, the time at which the fundraiser ends, and a list of contributors objects that include both the address and the contribution of each contributor.
pragma solidity ^0.4.24;
contract Fundraiser {
address public owner;
//target fundraising value
uint public target;
//time that fundraiser ends
uint public endTime;
//list of contributors
Contributor[] contributors;
struct Contributor{
address userAddress;
uint contribution;
}
The constructor of contracts is executed when the contract is created and can take arguments for the contract. In our case, we want to set the owner's address, the target goal, and the duration of the Fundraiser.
constructor(uint _target, uint duration) public payable {
owner = msg.sender;
target = _target;
endTime = now + duration;
}
Note that in Remix, to create a contract that takes in parameters, we must specify them when we "Deploy" it later.
You may be wondering where funds go, or how to "pay into" the smart contract. This is established by declaring a function "payable" as can be seen on the first line below. When a EOA sends funds to a smart contract they're authorizing it hold the money. In our case, that money will go to the owner of the contract if the goal is met. It's important to do logic checks here, do we want "now" to be greater than the the expiration of the contract? No! So we require that it is less than the last block's publication time ("now").
function contribute() public payable {
//require that fundraiser hasn't ended yet
require(now < endTime);
//add to list of contributors
contributors.push(Contributor(msg.sender,msg.value));
}
We need a function that the owner can call to receive the funds in the case that the fundraising goal is met. We can use the function "selfdestruct(address)
" to destroy the contract and send the funds held by the contract to the given address.
function collect() public{
//once target has been reached, owner can collect funds
require(address(this).balance >= target);
require(msg.sender == owner);
selfdestruct(owner);
}
We also need a function that can be called to refund contributors in the case that the fundraiser ends without the goal being reached. This can be done using a for loop to iterate through each contributor and refund the amount that they contributed.
function refund() public{
//allow refunds once time has ended if goal hasn't been met
require(now > endTime);
require(address(this).balance < target);
//refund all contributors
for(uint i; i<contributors.length;i++) {
contributors[i].userAddress.transfer(contributors[i].contribution);
}
}
We'd like to check the funds that have been paid into this smart contract. To do so, use the keyword "this", cast to an address and refer to it's balance field.
function balance() public view returns(uint){
return address(this).balance;
}
}
In Remix, compile the contract. Then, switch to the tab for deploying and running transactions. Our contract's constructor takes in two parameters: a target value for the fundraiser and a duration. Expand out the "Deploy" section by clicking on the drop-down icon on the right.
Then, specify a target of 100 Wei and a duration of 1 hour (3600 seconds) as shown below:
Click on "transact", confirm the transaction in Metamask, and wait for the contract to be deployed and the contract appears below with its address in Remix. Click on the left button to expand out the details of the deployed contract, including its functions.
Test out the fundraiser.
contribute()
function with the 50 Wei.collect()
- This should fail when you attempt to send the transaction.contribute()
another 50 Wei as before so that you meet the targetcollect()
- This should succeed since you've now met the target. Show the transaction in Etherscan that returns your money via SELFDESTRUCT.Now, try an unsuccessful fundraiser
contribute()
50 Wei to the contract endTime
has passedrefund().
Since the target has not been reached, a refund of 50 Wei should be sent back to your wallet. Show the transaction in Etherscan that returns your money via this call.Now we will implement a piggy bank smart contract for you to write your own Solidity code.
Below we have outlined the basic structure of the code, but we have replaced some of the code with ellipses (...). The functionality desired is as follows: Given a duration passed to the constructor, accept funds into the contract via the deposit function before the endTime
. Calculate the endTime
given duration. Do not allow any deposits outside of that timeframe. Implement a function to withdraw the funds once endTime
has been surpassed.
pragma solidity ^0.4.24;
contract TimedPiggyBank {
//owner of piggybank
address public owner;
//time that funds can be withdrawn
uint public endTime;
constructor(uint duration) public {
//store message sender as piggybank owner
...
//set endTime
...
}
//payable function that can be used to deposit funds
//leaving this blank will allow users to send funds without running any code
function deposit() public payable {}
function withdraw() public {
//make sure the message sender is the owner
...
//make sure that the current time is after endTime
...
//send funds to the owner and destroy the contract
...
}
function balance() public view returns(uint){
return address(this).balance;
}
}
In Remix, compile and deploy the contract. Then, after creating a piggy bank:
withdraw()
with different account - should failwithdraw()
before timer is over - should failwithdraw()
as owner after timer is over - should succeedInclude screenshots of the final set of contract transactions via Etherscan for your lab notebook. Transactions should include the initial deployment, the incremental deposit of funds, and the multiple attempts to withdraw funds.
We will now implement an escrow account smart contract. If you're unfamiliar with escrow accounts, they hold funds involved in a financial transaction until a third party determines that the terms of the contract are met, at which point the receiver of funds will be paid. The payee is an individual paying into the contract, the payer is waiting for some completed goal. The agent is a trusted third party that determines if the terms of the contract has been completed. The payer will either be refunded in the case that the terms are not completed by a certain expiration time, or the payee will be paid if the agent determines that the terms are completed. Find another classmate or use two accounts to test the functionality of your contract.
Below is a template of the Escrow contract. As before, fill in the missing parts to implement your contract.
pragma solidity ^0.4.24;
contract Escrow {
//creator of contract and sender of funds
address payer;
//receiver of funds, party that must finish terms
address payee;
//third party that determines when terms have been met
address agent;
//time after which sender can void the contract
uint expirationTime;
constructor(address _payee, address _agent, uint timeBeforeExpiration) public payable {
payee = _payee;
agent = _agent;
payer = msg.sender;
expirationTime = now + timeBeforeExpiration;
}
function voidContract() public {
//make sure the message sender is the payer
...
//make sure the contract has expired
...
//destroy contract and send funds to payer
...
}
function confirmCompletion() public {
//make sure agent is message sender
...
//destroy contract and send funds to payee
...
}
function balance() public view returns(uint){
return address(this).balance;
}
}
The Escrow contract requires multiple accounts to show how it works. For this part, ask a classmate to help you demonstrate that your code works. You may also create an additional wallet in Metamask and switch between them in order to show the contract works.
In Remix, compile and deploy the contract. Then, with multiple accounts or with a partner's account:
confirmCompletion()
with the payer account - should failvoidContract()
as payee - should failconfirmCompletion()
as agent - should succeedInclude screenshots of the final set of contract transactions via Etherscan to include in your lab notebook.
Commit your code as
/solidity/piggy/piggy.sol
for the TimedPiggyBank contract/solidity/escrow/escrow.sol
for the Escrow contractWithin each directory, also create a file called contract_address.txt
that has the address of your deployed contract
/solidity/piggy/contract_address.txt
/solidity/escrow/contract_address.txt
You've written and deployed your own smart contract code. Celebrate (or not).