Skip to content

Commit

Permalink
APT-913: Burnable Fungible Token reference implementation added #196
Browse files Browse the repository at this point in the history
* packages updated

* burnable zrc6 reference implementation added

* tests for burnable zrc2 added, tests refactored

* unused IsOwner removed
  • Loading branch information
lukozill authored Apr 16, 2024
1 parent 23a2d71 commit 40c00eb
Show file tree
Hide file tree
Showing 12 changed files with 5,034 additions and 4,549 deletions.
6,484 changes: 3,311 additions & 3,173 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
"format": "npx prettier --write ."
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@zilliqa-js/zilliqa": "3.3.4",
"jest": "^27.2.4",
"prettier": "^2.4.1",
"@types/jest": "^29.5.12",
"@zilliqa-js/zilliqa": "3.5.0",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"@zilliqa-js/scilla-json-utils": "0.2.0",
"ts-jest": "^27.0.5",
"typescript": "^4.4.4"
"ts-jest": "^29.1.2",
"typescript": "^5.4.5"
},
"type": "module",
"jest": {
Expand Down
224 changes: 224 additions & 0 deletions reference-contracts/FungibleToken-Burnable.scilla
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
scilla_version 0

(***************************************************)
(* Associated library *)
(***************************************************)
import IntUtils
library FungibleToken

let one_msg =
fun (msg : Message) =>
let nil_msg = Nil {Message} in
Cons {Message} msg nil_msg

let two_msgs =
fun (msg1 : Message) =>
fun (msg2 : Message) =>
let msgs_tmp = one_msg msg2 in
Cons {Message} msg1 msgs_tmp

(* Error events *)
type Error =
| CodeIsSender
| CodeInsufficientFunds
| CodeInsufficientAllowance

let make_error =
fun (result : Error) =>
let result_code =
match result with
| CodeIsSender => Int32 -1
| CodeInsufficientFunds => Int32 -2
| CodeInsufficientAllowance => Int32 -3
end
in
{ _exception : "Error"; code : result_code }

let zero = Uint128 0

(* Dummy user-defined ADT *)
type Unit =
| Unit

let get_val =
fun (some_val: Option Uint128) =>
match some_val with
| Some val => val
| None => zero
end

(***************************************************)
(* The contract definition *)
(***************************************************)

contract FungibleToken
(
contract_owner: ByStr20,
name : String,
symbol: String,
decimals: Uint32,
init_supply : Uint128
)

(* Mutable fields *)

field total_supply : Uint128 = init_supply

field balances: Map ByStr20 Uint128
= let emp_map = Emp ByStr20 Uint128 in
builtin put emp_map contract_owner init_supply

field allowances: Map ByStr20 (Map ByStr20 Uint128)
= Emp ByStr20 (Map ByStr20 Uint128)

(**************************************)
(* Procedures *)
(**************************************)

procedure ThrowError(err : Error)
e = make_error err;
throw e
end

procedure IsNotSender(address: ByStr20)
is_sender = builtin eq _sender address;
match is_sender with
| True =>
err = CodeIsSender;
ThrowError err
| False =>
end
end

procedure AuthorizedBurnIfSufficientBalance(from: ByStr20, amount: Uint128)
o_get_bal <- balances[from];
bal = get_val o_get_bal;
can_burn = uint128_le amount bal;
match can_burn with
| True =>
(* Subtract amount from from *)
new_balance = builtin sub bal amount;
balances[from] := new_balance;
current_total_supply <- total_supply;
new_total_supply = builtin sub current_total_supply amount;
total_supply := new_total_supply
| False =>
err = CodeInsufficientFunds;
ThrowError err
end
end

procedure AuthorizedMoveIfSufficientBalance(from: ByStr20, to: ByStr20, amount: Uint128)
o_from_bal <- balances[from];
bal = get_val o_from_bal;
can_do = uint128_le amount bal;
match can_do with
| True =>
(* Subtract amount from from and add it to to address *)
new_from_bal = builtin sub bal amount;
balances[from] := new_from_bal;
(* Adds amount to to address *)
get_to_bal <- balances[to];
new_to_bal = match get_to_bal with
| Some bal => builtin add bal amount
| None => amount
end;
balances[to] := new_to_bal
| False =>
(* Balance not sufficient *)
err = CodeInsufficientFunds;
ThrowError err
end
end

(***************************************)
(* Transitions *)
(***************************************)

(* @dev: Burn existing tokens. Only owner of token can burn *)
(* @param amount: Number of tokens to be burned. *)
transition Burn(amount: Uint128)
AuthorizedBurnIfSufficientBalance _sender amount;
e = {_eventname: "Burnt"; burner: _sender; burn_account: _sender; amount: amount};
event e;
msg_to_sender = {_tag : "BurnSuccessCallBack"; _recipient : _sender; _amount : zero;
burner : _sender; amount : amount};
msgs = one_msg msg_to_sender;
send msgs
end
(* @dev: Increase the allowance of an approved_spender over the caller tokens. Only token_owner allowed to invoke. *)
(* param spender: Address of the designated approved_spender. *)
(* param amount: Number of tokens to be increased as allowance for the approved_spender. *)
transition IncreaseAllowance(spender: ByStr20, amount: Uint128)
IsNotSender spender;
some_current_allowance <- allowances[_sender][spender];
current_allowance = get_val some_current_allowance;
new_allowance = builtin add current_allowance amount;
allowances[_sender][spender] := new_allowance;
e = {_eventname : "IncreasedAllowance"; token_owner : _sender; spender: spender; new_allowance : new_allowance};
event e
end

(* @dev: Decrease the allowance of an approved_spender over the caller tokens. Only token_owner allowed to invoke. *)
(* param spender: Address of the designated approved_spender. *)
(* param amount: Number of tokens to be decreased as allowance for the approved_spender. *)
transition DecreaseAllowance(spender: ByStr20, amount: Uint128)
IsNotSender spender;
some_current_allowance <- allowances[_sender][spender];
current_allowance = get_val some_current_allowance;
new_allowance =
let amount_le_allowance = uint128_le amount current_allowance in
match amount_le_allowance with
| True => builtin sub current_allowance amount
| False => zero
end;
allowances[_sender][spender] := new_allowance;
e = {_eventname : "DecreasedAllowance"; token_owner : _sender; spender: spender; new_allowance : new_allowance};
event e
end

(* @dev: Moves an amount tokens from _sender to the recipient. Used by token_owner. *)
(* @dev: Balance of recipient will increase. Balance of _sender will decrease. *)
(* @param to: Address of the recipient whose balance is increased. *)
(* @param amount: Amount of tokens to be sent. *)
transition Transfer(to: ByStr20, amount: Uint128)
AuthorizedMoveIfSufficientBalance _sender to amount;
e = {_eventname : "TransferSuccess"; sender : _sender; recipient : to; amount : amount};
event e;
(* Prevent sending to a contract address that does not support transfers of token *)
msg_to_recipient = {_tag : "RecipientAcceptTransfer"; _recipient : to; _amount : zero;
sender : _sender; recipient : to; amount : amount};
msg_to_sender = {_tag : "TransferSuccessCallBack"; _recipient : _sender; _amount : zero;
sender : _sender; recipient : to; amount : amount};
msgs = two_msgs msg_to_recipient msg_to_sender;
send msgs
end

(* @dev: Move a given amount of tokens from one address to another using the allowance mechanism. The caller must be an approved_spender. *)
(* @dev: Balance of recipient will increase. Balance of token_owner will decrease. *)
(* @param from: Address of the token_owner whose balance is decreased. *)
(* @param to: Address of the recipient whose balance is increased. *)
(* @param amount: Amount of tokens to be transferred. *)
transition TransferFrom(from: ByStr20, to: ByStr20, amount: Uint128)
o_spender_allowed <- allowances[from][_sender];
allowed = get_val o_spender_allowed;
can_do = uint128_le amount allowed;
match can_do with
| True =>
AuthorizedMoveIfSufficientBalance from to amount;
e = {_eventname : "TransferFromSuccess"; initiator : _sender; sender : from; recipient : to; amount : amount};
event e;
new_allowed = builtin sub allowed amount;
allowances[from][_sender] := new_allowed;
(* Prevent sending to a contract address that does not support transfers of token *)
msg_to_recipient = {_tag: "RecipientAcceptTransferFrom"; _recipient : to; _amount: zero;
initiator: _sender; sender : from; recipient: to; amount: amount};
msg_to_sender = {_tag: "TransferFromSuccessCallBack"; _recipient: _sender; _amount: zero;
initiator: _sender; sender: from; recipient: to; amount: amount};
msgs = two_msgs msg_to_recipient msg_to_sender;
send msgs
| False =>
err = CodeInsufficientAllowance;
ThrowError err
end
end
22 changes: 22 additions & 0 deletions tests/globalConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BN, Zilliqa, bytes, units } from "@zilliqa-js/zilliqa";
import Long from "long";

export const API = `http://localhost:${process.env["PORT"]}`; // Zilliqa Isolated Server
export const CHAIN_ID = 222;
export const MSG_VERSION = 1;
export const VERSION = bytes.pack(CHAIN_ID, MSG_VERSION);

export const JEST_WORKER_ID = Number(process.env["JEST_WORKER_ID"]);
export const GENESIS_PRIVATE_KEY = global.GENESIS_PRIVATE_KEYS[JEST_WORKER_ID - 1];

export const zilliqa = new Zilliqa(API);
zilliqa.wallet.addByPrivateKey(GENESIS_PRIVATE_KEY);

export const GAS_PRICE = units.toQa("2000", units.Units.Li);

export const FAUCET_PARAMS = {
version: VERSION,
amount: new BN(units.toQa("100000000", units.Units.Zil)),
gasPrice: GAS_PRICE,
gasLimit: Long.fromNumber(50),
};
126 changes: 126 additions & 0 deletions tests/testutils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { scillaJSONParams } from "@zilliqa-js/scilla-json-utils";
import { getAddressFromPrivateKey, schnorr } from "@zilliqa-js/zilliqa";
import { FAUCET_PARAMS, zilliqa } from "./globalConfig";

export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

export async function getAccounts(numberOfAccounts: number) {
const accounts = Array.from({ length: numberOfAccounts }, schnorr.generatePrivateKey).map(
(privateKey) => ({
privateKey,
address: getAddressFromPrivateKey(privateKey),
})
);

for (const { privateKey, address } of accounts) {
zilliqa.wallet.addByPrivateKey(privateKey);
const tx = await zilliqa.blockchain.createTransaction(
zilliqa.transactions.new(
{
...FAUCET_PARAMS,
toAddr: address,
},
false
)
);
if (!tx.getReceipt()?.success) {
throw new Error();
}
}

return accounts;
}

export type ContractTestCaseDefinition = {
name: string,
transition: string,
getSender: () => string,
getParams: () => Record<string, Array<unknown>>,
error: number | undefined,
want: {
expectState: (state: any) => void,
events: Array<{
name: string,
getParams: () => Record<string, Array<unknown>>,
}>,
transitions: Array<{
tag: string,
getParams: () => Record<string, Array<unknown>>,
}>,
} | undefined,
}

export const expectEvents = (events, want) => {
if (events === undefined) {
expect(undefined).toBe(want);
}

for (const [index, event] of events.entries()) {
expect(event._eventname).toBe(want[index].name);
const wantParams = scillaJSONParams(want[index].getParams());
expect(JSON.stringify(event.params)).toBe(JSON.stringify(wantParams));
}
};

export const expectTransitions = (
receiptTransitions: Array<{ msg: { params: any } }> | undefined,
expectedTransitions: Array<{
tag: string,
getParams: () => Record<string, Array<unknown>>,
}>
) => {
if (!receiptTransitions && expectedTransitions.length > 0) {
fail("Expected transitions but got none");
return;
}

expect(receiptTransitions!.length).toBe(expectedTransitions.length);

for (const [index, transition] of receiptTransitions!.entries()) {
const { msg } = transition;
expect(expectedTransitions[index]!.tag).toBe(expectedTransitions[index]!.tag);
const wantParams = scillaJSONParams(expectedTransitions[index]!.getParams());
expect(JSON.stringify(msg.params)).toBe(JSON.stringify(wantParams));
}
};

export const getErrorMsg = (code) =>
`Exception thrown: (Message [(_exception : (String "Error")) ; (code : (Int32 ${code}))])`;

export function runAllTestCases(
testCases: Array<ContractTestCaseDefinition>,
testedContractAddress: () => string,
txParams: any
) {
for (const testCase of testCases) {
it(`${testCase.transition}: ${testCase.name}`, async () => {
zilliqa.wallet.setDefault(testCase.getSender());
const tx: any = await zilliqa.contracts
.at(testedContractAddress())
.call(
testCase.transition,
scillaJSONParams(testCase.getParams()),
txParams
);

if (testCase.want === undefined) {
// Negative Cases
expect(tx.receipt.success).toBe(false);
expect(tx.receipt.exceptions[0].message).toBe(
getErrorMsg(testCase.error)
);
} else {
// Positive Cases
expect(tx.receipt.success).toBe(true);
expectEvents(tx.receipt.event_logs, testCase.want.events);
expectTransitions(tx.receipt.transitions, testCase.want.transitions);

const state = await zilliqa.contracts
.at(testedContractAddress())
.getState();

testCase.want.expectState(state);
}
});
}
}
Loading

0 comments on commit 40c00eb

Please sign in to comment.