Skip to main content

Implement Lottery Game Smart Contract

In this guide, we will implement the logic of our Lottery Game smart contract in the src/LotteryGame.cs file.

Step 1: Initializing Smart Contract Implementation

Replace the existing contents of the LotteryGame.cs file with the following code snippet:

using AElf.Contracts.MultiToken;
using AElf.Sdk.CSharp;
using AElf.Types;
using Google.Protobuf.WellKnownTypes;

namespace AElf.Contracts.LotteryGame
{
// Contract class must inherit the base class generated from the proto file
public class LotteryGame : LotteryGameContainer.LotteryGameBase
{
private const string TokenContractAddress = "ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx"; // tDVW token contract address
private const string TokenSymbol = "ELF";
private const long MinimumPlayAmount = 1_000_000; // 0.01 ELF
private const long MaximumPlayAmount = 1_000_000_000; // 10 ELF

// Initializes the contract
public override Empty Initialize(Empty input)
{
//TODO: Initialize token contract and owner
return new Empty();
}

// Plays the lottery game with a specified amount of tokens.
// The method checks if the play amount is within the limit.
// If the player wins, tokens are transferred from the contract to the sender and a PlayOutcomeEvent is fired with the won amount.
// If the player loses, tokens are transferred from the sender to the contract and a PlayOutcomeEvent is fired with the lost amount.
public override Empty Play(Int64Value input)
{
//TODO: Gamelogic and payout
return new Empty();
}

// Withdraws a specified amount of tokens from the contract.
// This method can only be called by the owner of the contract.
// After the tokens are transferred, a WithdrawEvent is fired to notify any listeners about the withdrawal.
public override Empty Withdraw(Int64Value input)
{
AssertIsOwner();
//TODO: Withdraw tokens from the contract token pool
return new Empty();
}

// Deposits a specified amount of tokens into the contract.
// This method can only be called by the owner of the contract.
// After the tokens are transferred, a DepositEvent is fired to notify any listeners about the deposit.
public override Empty Deposit(Int64Value input)
{
AssertIsOwner();
//TODO: Deposit tokens into the contract token pool
return new Empty();
}

// Transfers the ownership of the contract to a new owner.
// This method can only be called by the current owner of the contract.
public override Empty TransferOwnership(Address input)
{
AssertIsOwner();

// Set the new owner address
State.Owner.Value = input;

return new Empty();
}

// A method that read the contract's play amount limit
public override PlayAmountLimitMessage GetPlayAmountLimit(Empty input)
{
// Wrap the value in the return type
return new PlayAmountLimitMessage
{
MinimumAmount = MinimumPlayAmount,
MaximumAmount = MaximumPlayAmount
};
}

// A method that read the contract's current balance
public override Int64Value GetContractBalance(Empty input)
{
// Get the balance of the contract
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = Context.Self,
Symbol = TokenSymbol
}).Balance;

// Wrap the value in the return type
return new Int64Value
{
Value = balance
};
}

// A method that read the contract's owner
public override StringValue GetOwner(Empty input)
{
return State.Owner.Value == null ? new StringValue() : new StringValue {Value = State.Owner.Value.ToBase58()};
}

// Determines if the player is a winner.
// This method generates a random number based on the current block height and checks if it's equal to 0.
// If the random number is 0, the player is considered a winner.
private bool IsWinner()
{
var randomNumber = Context.CurrentHeight % 2;
return randomNumber == 0;
}

// This method is used to ensure that only the owner of the contract can perform certain actions.
// If the context sender is not the owner, an exception is thrown with the message "Unauthorized to perform the action."
private void AssertIsOwner()
{
Assert(Context.Sender == State.Owner.Value, "Unauthorized to perform the action.");
}
}

}
danger

In Lines 104 and 105, we utilize a basic and predictable random number generator. However, for scenarios involving assets, it's imperative to avoid such simplistic implementations.

Notice that some functions (e.g. Play) have their function bodies left relatively empty with a TODO. We will implement them in the steps below.

Step 2: Initializing Contract Reference State

Since we have declared our Contract Reference State, TokenContract, it is time to define it.

Replace the Initialize method in LotteryGame.cs with the following code snippet:

public override Empty Initialize(Empty input)
{
// Check if the contract is already initialized
Assert(State.Initialized.Value == false, "Already initialized.");
// Set the contract state
State.Initialized.Value = true;
// Set the owner address
State.Owner.Value = Context.Sender;

// Initialize the token contract
State.TokenContract.Value = Address.FromBase58(TokenContractAddress);

return new Empty();
}

Line 11 shows how you can reference any contract using the contract address. In this case, we have initialized TokenContract and will be able to use it to reference the MultiToken Contract in other functions to call methods.

tip

You may reference the name of any System Contracts by doing this (e.g. Multitoken Contract):
State.TokenContract.Value = Context.GetContractAddressByName(SmartContractConstants.TokenContractSystemName);

Step 3: Calling Contract Methods through Contract Reference State

We are going to make use of the TokenContract reference state in the Play method.

The logic of Play is as follows:

  1. Check that the amount played by the player does not exceed our limits per session.
  2. Check that the player has enough tokens.
  3. Check that the contract token pool has enough tokens.
  4. Roll to check if the player is the winner.
  5. Payout.

Replace the Play method in LotteryGame.cs with the following code snippet:

public override Empty Play(Int64Value input)
{
var playAmount = input.Value;

// Check if input amount is within the limit
Assert(playAmount is >= MinimumPlayAmount and <= MaximumPlayAmount, "Invalid play amount.");

// Check if the sender has enough tokens
var balance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = Context.Sender,
Symbol = TokenSymbol
}).Balance;
Assert(balance >= playAmount, "Insufficient balance.");

// Check if the contract has enough tokens
var contractBalance = State.TokenContract.GetBalance.Call(new GetBalanceInput
{
Owner = Context.Self,
Symbol = TokenSymbol
}).Balance;
Assert(contractBalance >= playAmount, "Insufficient contract balance.");

if(IsWinner())
{
// Transfer the token from the contract to the sender
State.TokenContract.Transfer.Send(new TransferInput
{
To = Context.Sender,
Symbol = TokenSymbol,
Amount = playAmount
});

// Emit an event to notify listeners about the outcome
Context.Fire(new PlayOutcomeEvent
{
Amount = input.Value,
Won = playAmount
});
}
else
{
// Transfer the token from the sender to the contract
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = TokenSymbol,
Amount = playAmount
});

// Emit an event to notify listeners about the outcome
Context.Fire(new PlayOutcomeEvent
{
Amount = input.Value,
Won = -playAmount
});
}

return new Empty();
}

Line 9: Shows how GetBalance of the Multitoken Contract is called to get the current balance of the player.
Line 27: Shows how to Transfer using the Multitoken Contract to transfer tokens from the contract to the player.
Line 44: Shows how to transfer tokens from the player to the contract through TransferFrom of the Multitoken Contract.

tip

Other methods of the Multitoken Contract can be found here.

Step 4: Implementing the Remaining Logic

You may choose to implement the rest of the functions as a practice, but we have provided a complete implementation below so that you can deploy and interact with your Lottery Game Contract.

Step 4.1: Withdraw Function

Replace the Withdraw method in LotteryGame.cs with the following code snippet:

public override Empty Withdraw(Int64Value input)
{
AssertIsOwner();

// Transfer the token from the contract to the sender
State.TokenContract.Transfer.Send(new TransferInput
{
To = Context.Sender,
Symbol = TokenSymbol,
Amount = input.Value
});

// Emit an event to notify listeners about the withdrawal
Context.Fire(new WithdrawEvent
{
Amount = input.Value,
From = Context.Self,
To = State.Owner.Value
});

return new Empty();
}

Step 4.2: Deposit Function

Replace the Deposit method in LotteryGame.cs with the following code snippet:

public override Empty Deposit(Int64Value input)
{
AssertIsOwner();

// Transfer the token from the sender to the contract
State.TokenContract.TransferFrom.Send(new TransferFromInput
{
From = Context.Sender,
To = Context.Self,
Symbol = TokenSymbol,
Amount = input.Value
});

// Emit an event to notify listeners about the deposit
Context.Fire(new DepositEvent
{
Amount = input.Value,
From = Context.Sender,
To = Context.Self
});

return new Empty();
}