Skip to content

Instantly share code, notes, and snippets.

@mariano-aguero
Last active April 14, 2020 16:52
Show Gist options
  • Save mariano-aguero/b9a4e6ee8d1b3e7a814a021febb2656e to your computer and use it in GitHub Desktop.
Save mariano-aguero/b9a4e6ee8d1b3e7a814a021febb2656e to your computer and use it in GitHub Desktop.
Liquidity pool for tezos
function natToTz(const val: nat): tez is val * 1tz
function natToMutez(const val: nat): tez is val * 1mutez
function tezToNatWithMutez(const val: tez): nat is val / 1mutez
function tezToNatWithTz(const val: tez): nat is val / 1tz
function natToInt(const val: nat): int is val + 0
function intToNat(const val: int): nat is block {
var res: nat := 0n;
if(val >= 0)
then res := abs(val)
else failwith("Not posible to convert from negative values")
} with res
// Naive iterative solution to calculate pow(x, n), would be to multiply x exactly n times
function pow (var baseNumber : int; var exponent : int) : int is
block {
var result: int := 1;
while exponent =/= 0 block {
result := result * baseNumber;
exponent := exponent - 1;
}
} with result;
#include "./utils/conversions.ligo"
#include "./utils/math.ligo"
#include "./partials/tokenActions.ligo"
type tokenInformation is record
contractAddress: address;
tokenDecimals: nat;
tokenSupply: nat;
end
type action is
| Deposit of (unit)
| Withdraw of (nat)
| Borrow of (nat)
| RepayBorrow of (unit)
| AddLiquidity of (unit)
| UpdateCollateralRate of (nat)
| GetExchangeRate of (unit * contract(nat))
| GetBalanceOf of (address * contract(tez))
type balanceInfo is record
tezAmount: tez;
blockTimestamp: timestamp;
end
type store is record
owner: address;
deposits: big_map(address, balanceInfo);
borrows: big_map(address, balanceInfo);
totalDeposits: tez;
totalBorrows: tez;
collateralRate: nat; // The collateral ratio that borrows must maintain (e.g. 2 implies 2:1), this represents the percentage of supplied value that can be actively borrowed at any given time.
liquidity: tez;
token: tokenInformation;
end
const emptyOps: list(operation) = list end;
type return is list(operation) * store;
function getExchangeRateInternal(var store: store): nat is
block {
var exchangeRate :nat := 1n;
if store.token.tokenSupply >= 1n
then block {
const realTokenSupply: nat = store.token.tokenSupply / intToNat(pow(10, natToInt(store.token.tokenDecimals)));
exchangeRate := tezToNatWithTz(store.totalDeposits + store.totalBorrows) / realTokenSupply;
}
else skip;
} with exchangeRate;
function getCoefficientInterest(var store: store): nat is
block {
var coefficientInterest :nat := 1n;
if (store.totalDeposits + store.totalBorrows) > 0tez
then coefficientInterest := store.totalBorrows / (store.totalDeposits + store.totalBorrows);
else skip;
} with coefficientInterest; attributes ["inline"];
function getBorrowInterestRate(var store: store): nat is (2n + getCoefficientInterest(store) * 20n);
function getDepositInterestRate(var store: store): nat is (getBorrowInterestRate(store) * getCoefficientInterest(store));
function calculateBorrowInterest(const accountInfo: balanceInfo; var store: store): tez is
block {
const anualBlocks: int = 522119;
const dailyBlocks: int = anualBlocks / 365;
const elapsedBlocks :int = now - accountInfo.blockTimestamp;
const interestAmount :tez = 0tez;
if elapsedBlocks > dailyBlocks
then block {
const elapsedDays :int = elapsedBlocks / dailyBlocks;
const interestRatePercentage :int = natToInt(getBorrowInterestRate(store) / 100n);
const powExp :int = elapsedDays / 365;
interestAmount := accountInfo.tezAmount * intToNat(pow((1 + interestRatePercentage), powExp));
}
else skip;
} with interestAmount; attributes ["inline"];
function calculateDepositInterest(const accountInfo: balanceInfo; var store: store): tez is
block {
const anualBlocks: int = 522119;
const dailyBlocks: int = anualBlocks / 365;
const elapsedBlocks :int = now - accountInfo.blockTimestamp;
const interestAmount :tez = 0tez;
if elapsedBlocks > dailyBlocks
then block {
const elapsedDays :int = elapsedBlocks / dailyBlocks;
const interestRatePercentage :int = natToInt(getDepositInterestRate(store) / 100n);
const powExp :int = elapsedDays / 365;
interestAmount := accountInfo.tezAmount * intToNat(pow((1 + interestRatePercentage), powExp));
}
else skip;
} with interestAmount; attributes ["inline"];
function tokenProxy (const action : tokenAction; const store : store): operation is
block {
const tokenContract: contract (tokenAction) =
case (Tezos.get_contract_opt (store.token.contractAddress) : option (contract (tokenAction))) of
Some (contractAction) -> contractAction
| None -> (failwith ("Contract not found.") : contract (tokenAction))
end;
const proxyOperation : operation = Tezos.transaction (action, 0mutez, tokenContract);
} with proxyOperation;
function getBorrow(var store: store): balanceInfo is
block {
var borrowsMap: big_map(address, balanceInfo) := store.borrows;
var borrow: option(balanceInfo) := borrowsMap[sender];
} with
case borrow of
| Some(borrowItem) -> borrowItem
| None -> record tezAmount = 0tez; blockTimestamp = now; end
end; attributes ["inline"];
function getDeposit(var store: store): balanceInfo is
block {
var depositsMap: big_map(address, balanceInfo) := store.deposits;
var deposit: option(balanceInfo) := depositsMap[sender];
} with
case deposit of
| Some(depositItem) -> depositItem
| None -> record tezAmount = 0tez; blockTimestamp = now; end
end; attributes ["inline"];
function updateDeposit(var amountToDeposit: tez; var store: store): store is
block {
var accountInfo: balanceInfo := getDeposit(store);
// calculate interest
const interest:tez = calculateDepositInterest(accountInfo, store);
store.deposits[sender] := record tezAmount = accountInfo.tezAmount + interest + amountToDeposit; blockTimestamp = now; end;
store.totalDeposits := store.totalDeposits + interest + amountToDeposit;
store.liquidity := store.liquidity + amountToDeposit;
} with store;
function updateBorrow(var amountToBorrow: tez; var store: store): store is
block {
var accountInfo: balanceInfo := getBorrow(store);
// calculate interest
const interest:tez = calculateBorrowInterest(accountInfo, store);
store.borrows[sender] := record tezAmount = accountInfo.tezAmount + interest + amountToBorrow; blockTimestamp = now; end;
store.totalBorrows := store.totalBorrows + interest + amountToBorrow;
store.liquidity := store.liquidity - amountToBorrow;
} with store;
function checkAccountLiquidity(var amountToValidate: nat; var store: store): unit is
block {
// account liquidity is defined as the total estimated tez value of an account's collateral
// supply balances multiplied by the protocol collateral rate factor,
// minus the total value of that account's borrow balances
var depositAccount: balanceInfo := getDeposit(store);
var borrowAccount: balanceInfo := getBorrow(store);
const depositAmountInInt: int = natToInt(tezToNatWithTz(depositAccount.tezAmount));
const borrowAmountInInt: int = natToInt(tezToNatWithTz(borrowAccount.tezAmount));
const amountOfCollateralAvailable: int = (depositAmountInInt * natToInt(store.collateralRate) / 100) - borrowAmountInInt + natToInt(amountToValidate);
const amountToValidateInTz: tez = natToTz(amountToValidate);
if amountOfCollateralAvailable < 0 or amountToValidateInTz >= store.liquidity
then failwith("Amount is greater than liquidity!");
else skip;
} with unit; attributes ["inline"];
function depositImp(var store: store): return is
block {
if amount = 0mutez
then failwith("No tez transferred!");
else skip;
// Setting the deposit to the sender
store := updateDeposit(amount, store);
// TODO: try to get the decimals property from the token contract
// The user receives a quantity of pTokens equal to the underlying tokens supplied, divided by the current Exchange Rate.
const amountToMintInNat: nat = intToNat(natToInt(tezToNatWithTz(amount) / getExchangeRateInternal(store)) * pow(10, natToInt(store.token.tokenDecimals)));
// Increment token supply
const newTokenSupply :nat = store.token.tokenSupply + amountToMintInNat;
patch store.token with record [tokenSupply = newTokenSupply];
// mintTo tokens to the senderAddress
const tokenProxyMintToOperation: operation = tokenProxy(MintTo(sender, amountToMintInNat), store);
const operations : list (operation) = list [tokenProxyMintToOperation];
} with(operations, store);
function withdrawImp(var amountToWithdraw: nat; var store: store): return is
block {
// If the amount is zero, failwith
if amountToWithdraw = 0n
then failwith("No amount to withdraw!");
else skip;
store := updateDeposit(0tez, store);
store := updateBorrow(0tez, store);
// Check account liquidity
checkAccountLiquidity(amountToWithdraw, store);
// Calculate amount to burn
const amountToBurnInNat: nat = intToNat(natToInt(amountToWithdraw / getExchangeRateInternal(store)) * pow(10, natToInt(store.token.tokenDecimals)));
// Decrement token supply
const newTokenSupply :nat = intToNat(store.token.tokenSupply - amountToBurnInNat);
patch store.token with record [tokenSupply = newTokenSupply];
// Burn pTokens
const tokenProxyBurnToOperation: operation = tokenProxy(BurnTo(sender, amountToBurnInNat), store);
// Update user's balance
const amountToWithdrawInTz: tez = natToTz(amountToWithdraw);
var accountInfo: balanceInfo := getDeposit(store);
store.deposits[sender] := record tezAmount = accountInfo.tezAmount - amountToWithdrawInTz; blockTimestamp = now; end;
store.totalDeposits := store.totalDeposits - amountToWithdrawInTz;
// Update liquidity
store.liquidity := store.liquidity - amountToWithdrawInTz;
// Create the operation to transfer tez to sender
const receiver : contract (unit) =
case (Tezos.get_contract_opt (sender): option(contract(unit))) of
Some (contractSender) -> contractSender
| None -> (failwith ("Not a contract") : (contract(unit)))
end;
const payoutOperation: operation = Tezos.transaction(unit, amountToWithdrawInTz, receiver);
const operations : list (operation) = list [tokenProxyBurnToOperation ; payoutOperation];
} with(operations, store);
function addLiquidity( var store : store) : return is
block {
if (sender =/= store.owner or amount = 0mutez)
then failwith("There was a problem trying to add liquidity");
else store.liquidity := store.liquidity + amount;
} with (emptyOps, store); attributes ["inline"];
function getExchangeRate (const callback : contract(nat); var store : store) : return is ( list [Tezos.transaction(getExchangeRateInternal(store), 0mutez, callback)], store); attributes ["inline"];
function getBalanceOf (const accountAddress: address; const callback : contract(tez); var store : store) : return is
block {
var operations: list(operation) := nil;
var depositsMap: big_map(address, balanceInfo) := store.deposits;
var accountInfo: option(balanceInfo) := depositsMap[accountAddress];
case accountInfo of
| None -> failwith("Account address not found")
| Some(accountInfoItem) -> operations := list [Tezos.transaction(accountInfoItem.tezAmount, 0mutez, callback)]
end;
} with (operations, store); attributes ["inline"];
function borrow(var amountToBorrow: nat; var store: store): return is
block {
if amountToBorrow = 0n
then failwith("No amount to borrow!");
else skip;
store := updateDeposit(0tez, store);
store := updateBorrow(0tez, store);
// Check account liquidity
checkAccountLiquidity(amountToBorrow, store);
// Setting the borrow to the sender
const amountToBorrowInTz: tez = natToTz(amountToBorrow);
store := updateBorrow(amountToBorrowInTz, store);
// Payout transaction to the sender address, with the amount to borrow
const receiver : contract (unit) =
case (Tezos.get_contract_opt (sender): option(contract(unit))) of
Some (contractSender) -> contractSender
| None -> (failwith ("Not a contract") : (contract(unit)))
end;
const operationTransfer: operation = Tezos.transaction(unit, amountToBorrowInTz, receiver);
const operations : list (operation) = list [operationTransfer];
} with(operations, store);
function repayBorrow(var store: store): return is
block {
if amount = 0mutez
then failwith("No tez transferred!");
else skip;
store := updateDeposit(0tez, store);
store := updateBorrow(0tez, store);
var accountInfo: balanceInfo := getBorrow(store);
// Check collateral ratio.
const borrowItemInNat: nat = tezToNatWithTz(accountInfo.tezAmount);
const borrowItemInTz: tez = natToTz(borrowItemInNat);
if amount > borrowItemInTz
then failwith("Amount to pay is greater than existing borrow amount!");
else skip;
// Update user's borrow balance
store.borrows[sender] := record tezAmount = accountInfo.tezAmount - amount; blockTimestamp = now; end;
store.totalBorrows := store.totalBorrows - amount;
// Update liquidity
store.liquidity := store.liquidity + amount;
} with(emptyOps, store);
function updateCollateralRate(const value : nat ; var store : store) : return is
block {
// Fail if is not the owner
if (sender =/= store.owner)
then failwith("You must be the owner of the contract to update the collateral ratio");
else store.collateralRate := value;
} with (emptyOps, store); attributes ["inline"];
function main (const action: action; var store: store): return is
block {
skip
} with case action of
| Deposit(n) -> depositImp(store)
| Withdraw(n) -> withdrawImp(n, store)
| Borrow(n) -> borrow(n, store)
| RepayBorrow(n) -> repayBorrow(store)
| UpdateCollateralRate(n) -> updateCollateralRate(n, store)
| AddLiquidity(n) -> addLiquidity(store)
| GetExchangeRate(n) -> getExchangeRate(n.1, store)
| GetBalanceOf(n) -> getBalanceOf(n.0, n.1, store)
end;
#include "./partials/tokenActions.ligo"
type account is record
balance: nat;
allowances: map(address, nat);
end
type store is record
owners: set(address);
decimals: nat; // Added this property used in the erc20 ethereum specification
symbol: string; // Added this property used in the erc20 ethereum specification
name: string; // Added this property used in the erc20 ethereum specification
totalSupply: nat;
accounts: big_map(address, account);
end
type return is list(operation) * store;
const emptyOps : list(operation) = list end;
const emptyAllowances : map(address,nat) = map end;
function getAccount (const addressAccount : address ; const accounts: big_map(address, account)) : account is
block { skip } with
case accounts[addressAccount] of
| Some(account) -> account
| None -> record balance = 0n; allowances = emptyAllowances; end
end
function getAllowance(const addressAccount : address ; const allowances : map(address, nat)) : nat is
block { skip } with
case allowances[addressAccount] of
| Some(value) -> value
| None -> 0n
end
function allowance (const addressOwner : address; const addressSpender : address; const callback : contract(nat); var store : store) : return is
block {
const storeAccountOwner: account = getAccount(addressOwner, store.accounts);
var allowed: nat := getAllowance(addressSpender, storeAccountOwner.allowances);
const allowedOperation: operation = Tezos.transaction(allowed, 0tz, callback);
operations := list
allowedOperation
end;
} with (operations, store);
function isAllowed (const addressOwner : address; const addressSpender : address; const value : nat; var store : store) : bool is
block {
const storeAccountOwner: account = getAccount(addressOwner, store.accounts);
var allowedAmount: nat := getAllowance(addressSpender, storeAccountOwner.allowances);
const isAllowed: bool = allowedAmount >= value;
} with isAllowed;
function isOwner (const addressOwner : address; var store : store) : bool is
block {
const isOwner : bool = store.owners contains addressOwner;
} with isOwner;
function updateOwners (var newAddress: address; var owners : set (address)) : set (address) is
block {
patch owners with set [newAddress];
} with owners;
function addOwner (const ownerAddress : address; var store : store) : return is
block {
case isOwner(sender, store) of
| False -> failwith ("Sender not allowed to perform this action")
| True -> skip
end;
store.owners := updateOwners(ownerAddress, store.owners);
} with (emptyOps, store);
function approve (const addressSpender : address; const value : nat; var store : store) : return is
block {
// If sender is the spender approving is not necessary
if sender = addressSpender then skip;
else block {
const senderAccount: account = getAccount(sender, store.accounts);
var allowed: nat := getAllowance(addressSpender, senderAccount.allowances);
// Changing allowance value from non-zero value to a non-zero value is forbidden to prevent the corresponding attack vector.
if allowed =/= 0n then failwith("UnsafeAllowanceChange");
else block {
// TODO: maybe this need a patch ?
senderAccount.allowances[addressSpender] := value;
store.accounts[sender] := senderAccount;
}
}
} with (emptyOps, store);
function transfer (const addressFrom : address; const addressTo : address; const value : nat; var store : store) : return is
block {
// #1 First check: when called with "from" account equal to the transaction sender, we assume that
// the user transfers their own money and this does not require approval.
if addressFrom = addressTo then skip;
else block {
// #2 NotEnoughAllowance: the transaction sender must be previously authorized to transfer at
// least the requested number of tokens from the "from" account using the approve
case isAllowed(addressFrom, addressTo, value, store) of
| False -> block {
failwith ("NotEnoughAllowance");
}
| True -> skip
end;
const addressFromAccount: account = getAccount(addressFrom, store.accounts);
// #3 NotEnoughBalance: check that the accountFrom can spend that much
if value > addressFromAccount.balance
then failwith ("NotEnoughBalance");
else skip;
// Update balances
const newFromBalance :nat = abs(addressFromAccount.balance - value); // ensure non negative
patch addressFromAccount with record [balance = newFromBalance];
store.accounts[addressFrom] := addressFromAccount;
const addressToAccount: account = getAccount(addressTo, store.accounts);
const newToBalance :nat = addressToAccount.balance + value; // ensure non negative
patch addressToAccount with record [balance = newToBalance];
store.accounts[addressTo] := addressToAccount;
// Update allowances
case store.accounts[addressFrom] of
| None -> skip
| Some(account) -> block {
case account.allowances[addressTo] of
| None -> skip
| Some(allowanceAmount) -> block {
account.allowances[addressTo] := abs(allowanceAmount - value);
store.accounts[addressFrom] := record balance = addressFromAccount.balance; allowances = account.allowances; end;
}
end;
}
end;
}
} with (emptyOps, store);
function mint (const value : nat ; var store : store) : return is
block {
// Fail if is not an owner
if not isOwner(sender, store) then failwith("You must be an owner of the contract to mint tokens");
else block {
var ownerAccount: account := record
balance = 0n;
allowances = (map end : map(address, nat));
end;
case store.accounts[sender] of
| None -> skip
| Some(n) -> ownerAccount := n
end;
// Update the owner balance and totalSupply
const newBalance :nat = ownerAccount.balance + value;
patch ownerAccount with record [balance = newBalance];
store.accounts[sender] := ownerAccount;
store.totalSupply := store.totalSupply + value;
}
} with (emptyOps, store)
function mintTo (const toAddress: address; const value : nat ; var store : store) : return is
block {
// Fail if is not an owner
if not isOwner(sender, store) then failwith("You must be an owner of the contract to mint tokens");
else block {
var toAccount: account := record
balance = 0n;
allowances = (map end : map(address, nat));
end;
case store.accounts[toAddress] of
| None -> skip
| Some(n) -> toAccount := n
end;
// Update the balance and totalSupply
const newBalance :nat = toAccount.balance + value;
patch toAccount with record [balance = newBalance];
store.accounts[toAddress] := toAccount;
store.totalSupply := store.totalSupply + value;
}
} with (emptyOps, store)
function burn (const value : nat ; var store : store) : return is
block {
// Fail if is not an owner
if not isOwner(sender, store) then failwith("You must be an owner of the contract to burn tokens");
else block {
var ownerAccount: account := record
balance = 0n;
allowances = (map end : map(address, nat));
end;
case store.accounts[sender] of
| None -> skip
| Some(n) -> ownerAccount := n
end;
// Check that the owner can spend that much
if value > ownerAccount.balance
then failwith ("NotEnoughBalance");
else skip;
// Check totalSupply
if value > store.totalSupply
then failwith ("TotalSupply is too low");
else skip;
// Update balances and totalSupply
const newBalance :nat = abs(ownerAccount.balance - value);
patch ownerAccount with record [balance = newBalance];
store.accounts[sender] := ownerAccount;
store.totalSupply := abs(store.totalSupply - value);
}
} with (emptyOps, store)
function burnTo (const toAddress: address ; const value : nat ; var store : store) : return is
block {
// Fail if is not an owner
if not isOwner(sender, store) then failwith("You must be an owner of the contract to burn tokens");
else block {
var toAccount: account := record
balance = 0n;
allowances = (map end : map(address, nat));
end;
case store.accounts[toAddress] of
| None -> skip
| Some(n) -> toAccount := n
end;
// Check that the owner can spend that much
if value > toAccount.balance
then failwith ("NotEnoughBalance");
else skip;
// Check totalSupply
if value > store.totalSupply
then failwith ("TotalSupply is too low");
else skip;
// Update balances and totalSupply
const newBalance :nat = abs(toAccount.balance - value);
patch toAccount with record [balance = newBalance];
store.accounts[toAddress] := toAccount;
store.totalSupply := abs(store.totalSupply - value);
}
} with (emptyOps, store)
function balanceOf (const addressOwner : address; const callback : contract(nat); var store : store) : return is
block {
const addressOwnerAccount: account = getAccount(addressOwner, store.accounts);
const addressOwnerBalance: nat = addressOwnerAccount.balance;
const addressOwnerBalanceOperation: operation = Tezos.transaction(addressOwnerBalance, 0tz, callback);
operations := list
addressOwnerBalanceOperation
end;
} with (operations, store);
function totalSupply (const callback : contract(nat); var store : store) : return is
block {
var totalSupply: nat := store.totalSupply;
const totalSupplyOperation: operation = transaction(totalSupply, 0mutez, callback);
operations := list
totalSupplyOperation
end;
} with (operations, store);
function decimals (const callback : contract(nat); var store : store) : return is
block {
var decimals: nat := store.decimals;
const decimalsOperation: operation = Tezos.transaction(decimals, 0mutez, callback);
operations := list
decimalsOperation
end;
} with (operations, store);
function name (const callback : contract(string); var store : store) : return is
block {
var name: string := store.name;
const nameOperation: operation = Tezos.transaction(name, 0mutez, callback);
operations := list
nameOperation
end;
} with (operations, store);
function symbol (const callback : contract(string); var store : store) : return is
block {
var symbol: string := store.symbol;
const symbolOperation: operation = Tezos.transaction(symbol, 0mutez, callback);
operations := list
symbolOperation
end;
} with (operations, store);
function main (const action : tokenAction ; const store : store) : return is
block {
if amount =/= 0tz then failwith ("This contract do not accept token amount");
else skip;
} with case action of
GetAllowance(n) -> allowance(n.0, n.1, n.2, store)
| Transfer(n) -> transfer(n.0, n.1, n.2, store)
| Approve(n) -> approve(n.0, n.1, store)
| GetBalance(n) -> balanceOf(n.0, n.1, store)
| GetTotalSupply(n) -> totalSupply(n.1, store)
| Mint(n) -> mint(n, store)
| MintTo(n) -> mintTo(n.0, n.1, store)
| Burn(n) -> burn(n, store)
| BurnTo(n) -> burnTo(n.0, n.1, store)
| AddOwner(n) -> addOwner(n, store)
| Decimals(n) -> decimals(n.1, store)
| Symbol(n) -> symbol(n.1, store)
| Name(n) -> name(n.1, store)
end;
type tokenAction is
| GetAllowance of (address * address * contract(nat))
| Transfer of (address * address * nat)
| Approve of (address * nat)
| GetBalance of (address * contract(nat))
| GetTotalSupply of (unit * contract(nat))
| Mint of (nat)
| MintTo of (address * nat)
| Burn of (nat)
| BurnTo of (address * nat)
| AddOwner of (address)
| Symbol of (unit * contract(string))
| Name of (unit * contract(string))
| Decimals of (unit * contract(nat))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment