Skip to main content

Refactoring

Smart contract refactoring

Just as any other project, smart contracts will need refactoring during their development. In this part, the way the winner is chosen will be refactored.

Everyone, by reading the code, can see that the winning ticket is 467 % Set.size(store.players). By tampering with the number of bought tickets, it is easy for anyone to get the winning ticket. In this part, we will make it harder to guess the winning ticker number. However, that method is not fully secured either. This refactoring is for educational purposes, to show some advanced features of LIGO and is NOT to be used in production.

This part is an opportunity to put the emphasis on two modules: Bytes and Crypto.

The Bytes module handles binary format for serialization, it converts Michelson structures into a binary format (and the reverse), concatenates two bytes. You can find a full reference here

The Crypto module performs a few basic operations such as hashing and verifying signatures. You can find a full reference here.

Winner selection scheme

Here is the procedure:

  1. The administrator will choose a large random number and keep it to himself.
  2. He hashes it and sends the hash when he calls the OpenRaffle entrypoint.
  3. This hash is saved into the storage.
  4. The administrator reveals his secret (random large number) when calling the CloseRaffle entrypoint.
  5. The smart contract hashes this number and checks that it matches the storage hash. If it does, it uses this number to pick the winner just as before.

As warned above, this method is still filled with loopholes:

  • the administrator knows the secret number and can tamper with the number of bought tickets to get the winning one.
  • everyone can try to brute-force the hash in order to find what number yielded this hash.

This method only makes it a little harder to guess the number.

Refactoring the OpenRaffle entrypoint

The OpenRaffle entrypoint expects a new input: the number hash, that should be saved into the storage. Both the storage and entrypoint have to be modified. The method is very similar to what has been done before:

  1. Refactoring the storage: it must store a hash. According to the LIGO documentation, a hash has a bytes type:
type storage ={
admin : address,
close_date : timestamp,
jackpot : tez,
description : string,
raffle_is_open : bool,
players : set<address>,
sold_tickets : big_map<nat, address>,
winning_ticket_number_hash : bytes
};
  1. Adding the new input in the openRaffleParameter. The bytes type is added in the tuple:
type openRaffleParameter = [tez, timestamp, option<string>,bytes];
  1. Updating the entrypoint function header:
const open_raffle = (jackpot_amount : tez,close_date : timestamp,description : option<string>, winning_ticket_number_hash : bytes,store : storage) : returnMainFunction => {
  1. Refactoring the entrypoint logic. For this change, the only thing to do is to save the hash in the storage:
const open_raffle = (jackpot_amount : tez,close_date : timestamp,description : option<string>, winning_ticket_number_hash : bytes,store : storage) : returnMainFunction => {
if(Tezos.get_source() != store.admin) return failwith("administrator not recognized");
else {
if(! store.raffle_is_open) {
if(Tezos.get_amount() < jackpot_amount) return failwith ("The administrator does not own enough tez.");
else {
const today : timestamp = Tezos.get_now();
const seven_day : int = 7 * 86400;
const in_7_day : timestamp = today + seven_day;
const is_close_date_not_valid : bool = close_date < in_7_day;

if(is_close_date_not_valid) return failwith("The raffle must remain open for at least 7 days.");
else {
let newStore = {
...store,
jackpot : jackpot_amount,
close_date : close_date,
raffle_is_open : true,
winning_ticket_number_hash : winning_ticket_number_hash // the hash is saved into the storage
};

return match(description,{
Some : (d) => [list([]),{...newStore,description:d}],
None : () => [list([]), store]
});
}
}
} else {
return failwith("A raffle is already open.");
}
}
};
  1. The new input has to be processed in the control flow:
const main = (action : raffleEntrypoints, store : storage):  returnMainFunction => {
return match(action,{
OpenRaffle : (param) => open_raffle(param[0], param[1], param[2], param[3], store),
BuyTicket : (param) => buy_ticket(param, store),
CloseRaffle: (param) => close_raffle (param, store)
});
};

You can compile the smart contract with:

ligo compile contract raffle.jsligo

Refactoring the CloseRaffle entrypoint

The method is the same here. So the step-by-step changes won't be detailed.

Try to do this refactoring as an exercice. The LIGO documentation will tell you how to hash a number and compare it. Once you're done with your smart contract refactoring, you can compare it with our suggested version:

// raffle.jsligo contract
type openRaffleParameter = [tez, timestamp, option<string>,bytes];
type buyTicketParameter = unit;
type closeRaffleParameter = nat;

type raffleEntrypoints =
| ["OpenRaffle",openRaffleParameter]
| ["BuyTicket", buyTicketParameter]
| ["CloseRaffle", closeRaffleParameter]
;

type storage ={
admin : address,
close_date : timestamp,
jackpot : tez,
description : string,
raffle_is_open : bool,
players : set<address>,
sold_tickets : big_map<nat, address>,
winning_ticket_number_hash : bytes
};

type returnMainFunction = [list<operation> , storage];

const div =(a : nat, b : nat) : option<nat> => {
if(b == (0 as nat)) return None();
else return Some(a/b);
};

const open_raffle = (jackpot_amount : tez,close_date : timestamp,description : option<string>, winning_ticket_number_hash : bytes,store : storage) : returnMainFunction => {
if(Tezos.get_source() != store.admin) return failwith("administrator not recognized");
else {
if(! store.raffle_is_open) {
if(Tezos.get_amount() < jackpot_amount) return failwith ("The administrator does not own enough tez.");
else {
const today : timestamp = Tezos.get_now();
const seven_day : int = 7 * 86400;
const in_7_day : timestamp = today + seven_day;
const is_close_date_not_valid : bool = close_date < in_7_day;

if(is_close_date_not_valid) return failwith("The raffle must remain open for at least 7 days.");
else {
let newStore = {
...store,
jackpot : jackpot_amount,
close_date : close_date,
raffle_is_open : true,
winning_ticket_number_hash : winning_ticket_number_hash // the hash is saved into the storage
};

return match(description,{
Some : (d) => [list([]),{...newStore,description:d}],
None : () => [list([]), store]
});
}
}
} else {
return failwith("A raffle is already open.");
}
}
};

const buy_ticket =(param: unit,store : storage) : returnMainFunction => {
if(store.raffle_is_open) {
const ticket_price : tez = 1 as tez;
const current_player : address = Tezos.get_sender();
if(Tezos.get_amount() != ticket_price) return failwith("The sender did not send the right tez amount.");
else {
if(Set.mem(current_player, store.players)) return failwith("Each player can participate only once.");
else {
const ticket_id : nat = Set.size(store.players);
let players : set<address> = Set.add(current_player, store.players);
return [list([]),{...store, players : players ,sold_tickets : Big_map.update(ticket_id, Some(current_player), store.sold_tickets)}];
}
}
} else {
return failwith("The raffle is closed.")
}
};

const close_raffle = (winning_ticket_number : nat, store : storage) : returnMainFunction => {
let operations : list<operation> = list([]);
if(Tezos.get_source() != store.admin) return failwith("Administrator not recognized.");
else {
if(store.raffle_is_open) {
if(Tezos.get_now() < store.close_date) return failwith("The raffle must remain open for at least 7 days.");
else {
const winning_ticket_number_bytes : bytes = Bytes.pack(winning_ticket_number);
const winning_ticket_number_hash : bytes = Crypto.sha256(winning_ticket_number_bytes);
if(winning_ticket_number_hash != store.winning_ticket_number_hash) return failwith("The hash does not match the hash of the winning ticket.");
else {
const number_of_players : nat = Set.size(store.players);
const random_number : nat = 467 as nat; // hardcoded number
const winning_ticket_id : nat = random_number % number_of_players; // modulo expression

const winner : address =
match(Big_map.find_opt(winning_ticket_id,store.sold_tickets) ,{
Some : (a) => a,
None : () => failwith("Winner address not found")
});

const receiver : contract<unit> =
match(Tezos.get_contract_opt(winner), {
Some : (c) => c,
None : () => failwith("Winner contract not found.")
});

const op : operation = Tezos.transaction(unit, store.jackpot, receiver);
operations = list([op]);

return [operations,
{...store,
jackpot : 0 as tez,
close_date : Tezos.get_now() as timestamp,
description : "raffle is currently closed",
raffle_is_open : false,
players : Set.empty as set<address>,
sold_tickets : Big_map.empty as big_map<nat, address>
}];
}
}
} else {
return failwith("The raffle is closed.");
}
}
};

const main = (action : raffleEntrypoints, store : storage): returnMainFunction => {
return match(action,{
OpenRaffle : (param) => open_raffle(param[0], param[1], param[2], param[3], store),
BuyTicket : (param) => buy_ticket(param, store),
CloseRaffle: (param) => close_raffle (param, store)
});
};

Conclusion

LIGO is meant for smart contract development and always yields a Michelson code. The method for developing such smart contracts is pretty much always the same, and follows an order very close to the Michelson smart contract structure containing:

  1. the parameter (or entrypoints): the entrypoints are defined into a variant, a type is defined for the input entrypoints.
  2. the storage: the storage is defined as a type, usually a record.
  3. the code: the main function dispatches the actions using a pattern matching. The logic for each entrypoint is implemented in a function.

There needs to be a main function, which dispatches the actions of the smart contract.

LIGO syntax was designed to help developers build smart contracts by providing them with syntax familiar to them: the main difference from other languages is the way the code is built and a few technical limitations due to the particularities of using a blockchain (randomness for instance).

LIGO is only a part of the tools that make the experience of smart contract development easier for developers. Another part, introduced later in this module, is unit testing.

To go further

To learn more about LIGO, you can take a look at:

  1. The official Ligolang documentation: a complete reference maintained by the developing team.
  2. Tezos Academy: a gamified interactive tutorial with examples.