Skip to content

Instantly share code, notes, and snippets.

@hdriqi
Created June 19, 2020 00:01
Show Gist options
  • Save hdriqi/87933a8cc6c133f89fd5a438a52edd91 to your computer and use it in GitHub Desktop.
Save hdriqi/87933a8cc6c133f89fd5a438a52edd91 to your computer and use it in GitHub Desktop.
const DEBUG = false; // set to `true` to surface debug log msgs throughout
/**
* EIP 20: ERC-20 Token Standard
*
* ERC-20 is a standard interface for tokens which allows for the implementation
* of a standard API for tokens within smart contracts. This standard provides
* basic functionality to transfer tokens, as well as allow tokens to be approved
* so they can be spent by another on-chain third party.
*
* A standard interface allows any tokens to be re-used by other applications,
* from wallets to decentralized exchanges.
*
* quoting https://eips.ethereum.org/EIPS/eip-20
* see more @ https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20
* see more @ https://github.com/ConsenSys/Tokens/blob/master/contracts/eip20/EIP20Interface.sol
*/
import {
u128, // extended number type for working with large numbers
logging, // append log messages to VM logging, exposed via `logging.log()` to JS Dev console and via `logs()` in mock VM
context, // access to contract context for sender, attachedDeposit and others. see https://github.com/near/near-sdk-as/blob/master/assembly/runtime/contract.ts
storage, // key-value store representing contract state on the blockchain
PersistentMap, // convenience wrapper around storage that mimics a strongly typed map
PersistentDeque
} from "near-sdk-as";
// ----------------------------------------------------------------------------
// this file contains models representing events emitted by the contract
// ----------------------------------------------------------------------------
/**
* originally defined in https://eips.ethereum.org/EIPS/eip-20 as
* event Transfer(address indexed _from, address indexed _to, uint256 _value);
*
* modifications (additional field `spender`) applied after reading
* https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit
*
*
* Transfer
* MUST trigger when tokens are transferred, including zero value transfers.
*
* A token contract which creates new tokens SHOULD trigger a Transfer event
* with the _from address set to 0x0 when tokens are created.
*
* event Transfer(address indexed _from, address indexed _to, uint256 _value)
*
*/
export class TransferEvent {
constructor(
public spender: string,
public from: string,
public to: string,
public value: u128) { }
}
/**
* originall defined in https://eips.ethereum.org/EIPS/eip-20 as
* event Approval(address indexed _owner, address indexed _spender, uint256 _value);
*
* modifications (additional field `oldValue`) applied after reading
* https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit
*
* Approval
*
* MUST trigger on any successful call to approve(address _spender, uint256 _value).
*
* event Approval(address indexed _owner, address indexed _spender, uint256 _value)
*
*/
@nearBindgen
export class ApprovalEvent {
constructor(
public owner: string,
public spender: string,
public oldValue: u128,
public value: u128) { }
}
// setup a queue for transfer events
const transferEvents = new PersistentDeque<TransferEvent>("xfr");
// setup a queue for approval events
const approvalEvents = new PersistentDeque<ApprovalEvent>("apr");
/**
* This function records transfer events since NEAR doesn't currently support
* an event model on-chain
*
* @param spender
* @param from
* @param to
* @param value
*/
export function recordTransferEvent(spender: string, from: string, to: string, value: u128): void {
DEBUG ? logging.log("[call] recordTransferEvent(" + spender + ", " + from + ", " + to + ", " + value.toString() + ")") : false;
const transfer = new TransferEvent(spender, from, to, value);
transferEvents.pushFront(transfer)
}
/**
* this function returns the very first recorded transfer event
*
* mutates the list of transfer events permanently by removing the earliest
* event from storage
*/
export function getOldestTransferEvent(): TransferEvent {
DEBUG ? logging.log("[call] getOldestTransferEvent()") : false;
return transferEvents.popBack();
}
/**
* this function returns the most recently recorded transfer event
*
* mutates the list of transfer events permanently by removing the most recent
* event from storage
*/
export function getNewestTransferEvent(): TransferEvent {
DEBUG ? logging.log("[call] getNewestTransferEvent()") : false;
return transferEvents.popFront();
}
/**
* This function records approval events since NEAR doesn't currently support
* an event model on-chain
*
* @param owner
* @param spender
* @param oldValue
* @param value
*/
export function recordApprovalEvent(owner: string, spender: string, oldValue: u128, value: u128): void {
DEBUG ? logging.log("[call] recordApprovalEvent(" + owner + ", " + spender + ", " + oldValue.toString() + ", " + value.toString() + ")") : false;
const approval = new ApprovalEvent(owner, spender, oldValue, value);
approvalEvents.pushFront(approval)
}
/**
* this function returns the very first recorded approval event
*
* mutates the list of approval events permanently by removing the earliest
* event from storage
*/
export function getOldestApprovalEvent(): ApprovalEvent {
DEBUG ? logging.log("[call] getOldestApprovalEvent()") : false;
return approvalEvents.popBack();
}
/**
* this function returns the most recently recorded approval event
*
* mutates the list of approval events permanently by removing the most recent
* event from storage
*/
export function getNewestApprovalEvent(): ApprovalEvent {
DEBUG ? logging.log("[call] getNewestApprovalEvent()") : false;
return approvalEvents.popFront();
}
// ----------------------------------------------------------------------------
// OPTIONAL
// ----------------------------------------------------------------------------
/*
name
Returns the name of the token - e.g. "MyToken".
OPTIONAL - This method can be used to improve usability, but interfaces and
other contracts MUST NOT expect these values to be present.
function name() public view returns (string)
*/
export function name(): string {
if (!initialized) initialize() // enforce auto-initialization
// if name has been customized, use it. otherwise use default
const name = storage.getSome<string>("_name");
DEBUG ? logging.log("[status] Token.name: " + name) : false;
return name;
}
/*
symbol
Returns the symbol of the token. E.g. "HIX".
OPTIONAL - This method can be used to improve usability, but interfaces and
other contracts MUST NOT expect these values to be present.
function symbol() public view returns (string)
*/
export function symbol(): string {
if (!initialized) initialize() // enforce auto-initialization
// if symbol has been customized, use it. otherwise use default
const symbol = storage.getSome<string>("_symbol");
DEBUG ? logging.log("[status] Token.symbol: " + symbol) : false;
return symbol;
}
/*
decimals
Returns the number of decimals the token uses - e.g. 8, means to divide the
token amount by 100000000 to get its user representation.
OPTIONAL - This method can be used to improve usability, but interfaces and
other contracts MUST NOT expect these values to be present.
function decimals() public view returns (uint8)
*/
export function decimals(): u8 {
if (!initialized) initialize() // enforce auto-initialization
// if decimals has been customized, use it. otherwise use default
const decimals: u8 = storage.getSome<u8>("_decimals");
DEBUG ? logging.log("[status] Token.decimals: " + decimals.toString()) : false;
return decimals;
}
// ----------------------------------------------------------------------------
// REQUIRED
// ----------------------------------------------------------------------------
/**
* totalSupply
* Returns the total token supply.
*
* function totalSupply() public view returns (uint256)
*/
export function totalSupply(): u128 {
if (!initialized) initialize() // enforce auto-initialization
// if totalSupply has been customized, use it. otherwise use default
const totalSupply: u128 = storage.getSome<u128>("_totalSupply");
DEBUG ? logging.log("[status] Token.supply: " + totalSupply.toString()) : false;
return totalSupply;
}
/**
* balanceOf
* Returns the account balance of another account with address _owner.
*
* function balanceOf(address _owner) public view returns (uint256 balance)
*
* @param _owner The address from which the balance will be retrieved
* @return The balance
*/
export function balanceOf(owner: string): u128 {
if (!initialized) initialize() // enforce auto-initialization
DEBUG ? logging.log("[call] balanceOf(" + owner + ")") : false;
// let balance: u128 = balances.getSome(owner);
// let balance: u128 = <u128>balances.get(owner, u128.fromI32(0))!;
// let balance: u128 = <u128>balances.get(owner, u128.from(0))!;
// let balance: u128 = balances.get(owner, u128.Zero)!;
let balance: u128 = <u128>balances.get(owner, u128.Zero);
DEBUG ? logging.log("[status] " + owner + " has balance " + balance.toString()) : false;
return balance
}
/**
* transfer
* Transfers _value amount of tokens to address _to, and MUST fire the Transfer
* event. The function SHOULD throw if the message caller’s account balance
* does not have enough tokens to spend.
*
* Note Transfers of 0 values MUST be treated as normal transfers and fire the
* Transfer event.
*
* function transfer(address _to, uint256 _value) public returns (bool success)
*
* @notice send `value` token to `to` from `context.sender`
* @param to The address of the recipient
* @param value The amount of token to be transferred
* @return Whether the transfer was successful or not
*/
export function transfer(to: string, value: u128): boolean {
if (!initialized) initialize() // enforce auto-initialization
DEBUG ? logging.log("[call] transfer(" + to + ", " + value.toString() + ")") : false;
const sender = context.sender;
const recipient = to;
// sender account must exist and have tokens
assert(sender, "Sender can not be blank")
assert(balances.contains(sender), "Sender balance cannot be zero")
// fetch balances for sender and recipient
const senderBalance = <u128>balances.get(sender, u128.Zero);
const recipientBalance = <u128>balances.get(to, u128.Zero);
// sender tokens must be greater than or equal to value being transferred
assert(senderBalance >= value, "Sender has insufficient funds for transfer");
// move tokens among accounts
balances.set(sender, u128.sub(senderBalance, value));
balances.set(recipient, u128.add(recipientBalance, value));
// record the transfer event
let spender = sender;
recordTransferEvent(spender, spender, to, value);
return true;
}
/**
* transferFrom
* Transfers `value` amount of tokens from address `from` to address `to`, and
* MUST fire the `Transfer` event.
*
* The transferFrom method is used for a withdraw workflow, allowing contracts
* to transfer tokens on your behalf. This can be used for example to allow a
* contract to transfer tokens on your behalf and/or to charge fees in
* sub-currencies. The function SHOULD throw unless the _from account has
* deliberately authorized the sender of the message via some mechanism.
*
* Note Transfers of 0 values MUST be treated as normal transfers and fire the
* Transfer event.
*
* function transferFrom(address _from, address _to, uint256 _value) public returns
*
* @param from The address of the sender
* @param to The address of the recipient
* @param value The amount of token to be transferred
* @returns Whether the transfer was successful or not
*/
export function transferFrom(from: string, to: string, value: u128): boolean {
if (!initialized) initialize() // enforce auto-initialization
DEBUG ? logging.log("[call] transferFrom(" + from + ", " + to + ", " + value.toString() + ")") : false;
const owner = from;
const spender = context.sender;
// spender account must exist and be authorized to transfer funds
assert(spender, "Spender can not be blank")
// spender must be allowed to transfer this amount
assert(allowance(owner, spender) >= value, "Spender is not authorized to transfer amount")
// fetch balances for sender and recipient
const fromBalance = <u128>balances.get(from, u128.Zero);
const recipientBalance = <u128>balances.get(to, u128.Zero);
// sender tokens must be greater than or equal to value being transferred
assert(fromBalance >= value, "From account has insufficient funds for transfer");
// move tokens among accounts
balances.set(from, u128.sub(fromBalance, value));
balances.set(to, u128.add(recipientBalance, value));
// decrement allowance by transferred amount as well
decrementAllowance(owner, spender, value)
// record the transfer event
recordTransferEvent(spender, from, to, value);
return true;
}
/**
* approve
* Allows `spender` to withdraw from your account multiple times, up to the
* `value` amount. If this function is called again it overwrites the current
* allowance with `value`.
*
* NOTE: To prevent attack vectors like the ones described in the original spec,
* clients SHOULD make sure to create user interfaces in such a way that
* they set the allowance first to 0 before setting it to another value for the
* same spender. THOUGH The contract itself shouldn’t enforce it, to allow
* backwards compatibility with contracts deployed before
*
* function approve(address _spender, uint256 _value) public returns (bool success)
*
* @param address The address of the account able to transfer the tokens
* @param value The amount of tokens to be approved for transfer
* @returns Whether the approval was successful or not
*/
export function approve(spender: string, value: u128): boolean {
if (!initialized) initialize() // enforce auto-initialization
DEBUG ? logging.log("[call] approve(" + spender + ", " + value.toString() + ")") : false;
// get owner balance
const owner = context.sender;
const balance = <u128>balances.get(owner, u128.Zero);
// owner must have enough balance to approve this value
assert(balance >= value, "Owner has insufficient funds for approval")
// construct key in collection of allowances and fetch old allowance
const allowancesKey = getAllowancesKey(owner, spender);
const oldValue = <u128>allowances.get(allowancesKey, u128.Zero);
// save or update allowance
allowances.set(allowancesKey, value)
// record the approval event
recordApprovalEvent(owner, spender, oldValue, value);
return true;
}
/**
* allowance
* Returns the amount which `spender` is still allowed to withdraw from `owner`.
*
* function allowance(address _owner, address _spender) public view returns (uint256 remaining)
*
* @param owner The address of the account owning tokens
* @param spender The address of the account able to transfer the tokens
* @return Amount of remaining tokens allowed to spent
*/
export function allowance(owner: string, spender: string): u128 {
if (!initialized) initialize() // enforce auto-initialization
DEBUG ? logging.log("[call] allowance(" + owner + ", " + spender + ")") : false;
// construct key in collection of allowances and return allowance
const allowancesKey = getAllowancesKey(owner, spender);
return <u128>allowances.get(allowancesKey, u128.Zero)
}
/**
* Helper function to decrement allowance
*
* @param owner The address of the account owning tokens
* @param spender The address of the account able to transfer the tokens
* @param value Amount
*/
function decrementAllowance(owner: string, spender: string, spent: u128): void {
const allowancesKey = getAllowancesKey(owner, spender);
const allowance = allowances.getSome(allowancesKey);
const remaining = u128.sub(allowance, spent);
allowances.set(allowancesKey, remaining);
}
/**
* Helper function to standardize the mapping
* This function would not be needed if we could embed a PersistentMap as the value of another PersistentMap
*
* @param owner of the account from which tokens will be spent
* @param spender of the tokens in the owners account
*/
function getAllowancesKey(owner: string, spender: string): string {
const separator: string = ":"
return owner + separator + spender
}
// ----------------------------------------------------------------------------
// BOOK KEEPING
// ----------------------------------------------------------------------------
/**
* balances of all accounts in the system. this is the single source of truth for balances
*/
const balances = new PersistentMap<string, u128>("bal"); // map[owner] = balance
/**
* allowances of all accounts in the system. this is the source of truth for allowed spending
*/
// const allowances = new PersistentMap<string, PersistentMap<string, u128>>("a"); // map[owner][spender] = allowance
const allowances = new PersistentMap<string, u128>("alw"); // map[owner:spender] = allowance
// ----------------------------------------------------------------------------
// EXTENDED FUNCTIONALITY
// ----------------------------------------------------------------------------
/**
* This function supports the customization of this ERC-20 token before initialization
* It may or may not be called. If not called, the contract uses sensible defaults
* If called, it can only be called once (!) and prevents repeat calls
*
* NOTE: this function uses storage keys with _underscore prefix since these are guaranteed not
* to conflict with accounts on the NEAR platform. see https://nomicon.io/DataStructures/Account.html#examples
*
* THIS IS NOT part of the ERC-20 spec
*
* @param name of the token
* @param symbol for the token
* @param decimals places used when rendering the token
* @param supply of the tokens in total at launch
*/
//
let customized = false
export function mint(to: string, supply: u128): boolean {
// block this function from being called twice
if (customized) return true;
assertTrueOwner()
const currentSupply = storage.getSome<u128>("_totalSupply")
const newSupply = u128.add(currentSupply, supply)
storage.set("_totalSupply", newSupply);
balances.set(to, supply);
// block this function from being called twice
customized = true;
return true;
}
export function customize(
name: string = "Solidus Wonder Token", // awesome name for a token
symbol: string = "SWT", // pronounced "sweet", rhymes with "treat"
decimals: u8 = 2, // number of decimal places to assume for rendering,
supply: u128 = u128.from(100_000_000), // <raised pinky> one meeeeellion coins ... divisible in 100ths
): boolean {
// block this function from being called twice
if (customized) return true;
const owner = assertTrueOwner()
// only set values that are provided, otherwise ignore
storage.set("_bank", owner);
storage.set("_name", name);
storage.set("_symbol", symbol);
storage.set("_decimals", decimals);
storage.set("_totalSupply", supply);
// block this function from being called twice
customized = true;
return customized;
}
/**
* This function initializes the token and assigns the configured total supply to a single account
* This function may only be called once and prevents subsequent calls
*
* THIS IS NOT part of the ERC-20 spec
*/
let initialized = false
export function initialize(): boolean {
if (initialized) return true; // block this function from being called twice
if (!customized) customize(); // use defaults if not customized on initialization
// make sure the caller is the account that owns the contract
const owner = assertTrueOwner()
storage.set("_bank", owner)
// transfer initial supply to initial owner
const initialSupply: u128 = storage.getSome<u128>("_totalSupply");
balances.set(owner, initialSupply);
DEBUG ? logging.log("[status] Initial owner: " + owner) : false;
// record the transfer event
recordTransferEvent("0x0", "0x0", owner, initialSupply);
// block this function from being called twice
initialized = true;
return initialized;
}
// /**
// * This function supports exchanging NEAR tokens for this ERC-20 token
// * at the configured rate of exchange
// *
// * THIS IS NOT part of the ERC-20 spec
// */
// export function exchange(): u128 {
// // grab the sender and their attached deposit
// const sender = context.sender
// const deposit = context.attachedDeposit
// const exchange = getExchangeRate()
// // convert the value using the exchange rate
// const value = u128.mul(deposit, u128.from(exchange));
// const bank: string = storage.getSome<string>("_bank");
// // make sure the bank has enough to cover this exchange
// assert(value < balanceOf(bank), "Not enough tokens available for this exchange")
// // transfer attached deposit supply to initial owner
// transfer(sender, value)
// DEBUG ? logging.log("[status] Initial owner: " + bank) : false;
// return value
// }
/**
* a guard clause to prevent any account but the token contract itself from
* invoking some methods
*
* THIS IS NOT part of the ERC-20 spec
*/
function assertTrueOwner(): string {
// only allow the contract account to invoke this guard clause
const owner = context.sender
// the contract name must be available
assert(context.contractName, "Permission denied: ERR001")
// the sender of this transaction must be the same account
assert(owner == context.contractName, "Permission denied: ERR002")
return owner
}
import { context, storage, env, util, ContractPromiseBatch, u128 } from "near-sdk-as";
export function setContractToDeploy(): void {
env.input(0)
const contract = util.read_register(0)
storage.set<Uint8Array>("code", contract)
}
@nearBindgen
class Args {
name: string
symbol: string
decimals: u8
totalSuppy: u128
}
export function createContract(name: string, symbol: string, decimals: u8): string {
const contractName = name.concat('.').concat(context.sender)
const contractCode = storage.getSome<Uint8Array>("code")
const args: Args = {
name: name,
symbol: symbol,
decimals: decimals,
totalSuppy: u128.Zero
}
ContractPromiseBatch
.create(contractName)
.create_account()
.deploy_contract(contractCode)
.function_call(
"customize",
args.encode(),
u128.Zero,
0
)
return contractName
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment