Token Contract Sample - HcashOrg/Hcash GitHub Wiki

HCash's token contract's standard is similar with ERC20.

A standard HCash token contract must have APIs and offline APIs below:

required APIs:
    transfer:   transfer to another address, the argument format is "to_address,amount_with_precision"
    approve:    approve some token amount to other address. allow the destination address to spend those tokens
    transferFrom:     transfer to another address from approved token balance, the argument format is "fromAddress,toAddress,amount_with_precision"


required offline APIs:
    tokenName: query the name of the token, eg. hcash
    tokenSymbol:  query the symbol of the token, eg. HSR
    precision:   query precision of the token, eg. 10000 means the least accurate asset amount is 0.0001
    admin:   the creator or administrator of the token contract
    balanceOf:   query token balance of address
    approvedBalanceFrom:   query remaining approved amounts from user, argument format is "spenderAddress,authorizerAddress"
    allApprovedFromUser:   query all remaining approved amounts from user, argument format is "fromAddress"

A sample token contract is below:


type State = 'NOT_INITED' | 'COMMON' | 'PAUSED' | 'STOPPED'

type Storage = {
    name: string,
    symbol: string,
    supply: int,
    precision: int, -- only used to display
    users: Map<int>,
    allowed: Map<string>, -- approved amounts, each item is a json string of userAddress=>amount
    lockedAmounts: Map<string>, -- userAddress => "lockedAmount,unlockBlockNumber"
    state: string,
    allowLock: bool,
    admin: string -- admin user address
}

-- events: Transfer, Paused, Resumed, Stopped, AllowedLock, Locked, Unlocked

var M = Contract<Storage>()

function M:init()
    self.storage.name = ''
    self.storage.symbol = ''
    self.storage.supply = 0
    self.storage.precision = 0
    self.storage.users = {}
    self.storage.allowed = {}
    self.storage.lockedAmounts = {}
    self.storage.state = 'NOT_INITED'
    self.storage.admin = caller_address
    self.storage.allowLock = false
end

let function checkAdmin(self: table)
    if self.storage.admin ~= caller_address then
        return error("you are not admin, can't call this function")
    end
end

-- parse a,b,c format string to [a,b,c]
let function parse_args(arg: string, count: int, error_msg: string)
    if not arg then
        return error(error_msg)
    end
    let parsed = string.split(arg, ',')
    if (not parsed) or (#parsed ~= count) then
        return error(error_msg)
    end
    return parsed
end

let function parse_at_least_args(arg: string, count: int, error_msg: string)
    if not arg then
        return error(error_msg)
    end
    let parsed = string.split(arg, ',')
    if (not parsed) or (#parsed < count) then
        return error(error_msg)
    end
    return parsed
end

let function arrayContains(col: Array<object>, item: object)
    if not item then
        return false
    end
    var value: object
    for _, value in ipairs(col) do
        if value == item then
            return true
        end
    end
    return false
end

function M:on_deposit(amount: int)
    return error("not support deposit to token")
end

let function get_from_address()
    var from_address: string
    let prev_contract_id = get_prev_call_frame_contract_address()
    if prev_contract_id and is_valid_contract_address(prev_contract_id) then
        from_address = prev_contract_id
    else
        from_address = caller_address
    end
    return from_address
end

-- arg: name,symbol,supply,precision
function M:init_token(arg: string)
    checkAdmin(self)
    pprint('arg:', arg)
    if self.storage.state ~= 'NOT_INITED' then
        return error("this token contract inited before")
    end
    let parsed = parse_args(arg, 4, "argument format error, need format: name,symbol,supply,precision")
    let info = {name: parsed[1], symbol: parsed[2], supply: tointeger(parsed[3]), precision: tointeger(parsed[4])}
    if not info.name then
        return error("name needed")
    end
    self.storage.name = tostring(info.name)
    if not info.symbol then
        return error("symbol needed")
    end
    self.storage.symbol = tostring(info.symbol)
    if not info.supply then
        return error("supply needed")
    end
    let supply = tointeger(info.supply)
    if (not supply) or (supply <= 0) then
        return error("supply must be positive integer")
    end
    self.storage.supply = supply

    let from_address = get_from_address()
    if from_address ~= caller_address then
        return error("init_token can't be called from other contract")
    end

    self.storage.users[caller_address] = supply

    if not info.precision then
        return error("precision needed")
    end
    let precision = tointeger(info.precision)
    if (not precision) or (precision <= 0) then
        return  error("precision must be positive integer")
    end
    let allowedPrecisions = [1,10,100,1000,10000,100000,1000000,10000000,100000000]
    if not (arrayContains(allowedPrecisions, precision)) then
        return error("precision can only be positive integer in " .. json.dumps(allowedPrecisions))
    end
    self.storage.precision = precision
    self.storage.state = 'COMMON'
    let supplyStr = tostring(supply)
    emit Inited(supplyStr)
end

let function checkState(self: table)
    if self.storage.state == 'NOT_INITED' then
        return error("contract token not inited")
    end
    if self.storage.state == 'PAUSED' then
        return error("contract paused")
    end
    if self.storage.state == 'STOPPED' then
        return error("contract stopped")
    end
end

let function checkStateInited(self: table)
    if self.storage.state == 'NOT_INITED' then
        return error("contract token not inited")
    end
end

let function checkAddress(addr: string)
    let result = is_valid_address(addr)
    if not result then
        return error("address format error")
    end
    return result
end

offline function M:state(arg: string)
    return self.storage.state
end

offline function M:tokenName(arg: string)
    checkStateInited(self)
    return self.storage.name
end

offline function M:precision(_: string)
    checkStateInited(self)
    return self.storage.precision
end

offline function M:tokenSymbol(arg: string)
    checkStateInited(self)
    return self.storage.symbol
end

offline function M:admin(_: string)
    checkStateInited(self)
    return self.storage.admin
end

offline function M:totalSupply(arg: string)
    checkStateInited(self)
    return self.storage.supply
end

offline function M:isAllowLock(_: string)
    let resultStr = tostring(self.storage.allowLock)
    return resultStr
end

function M:openAllowLock(_: string)
    checkAdmin(self)
    checkState(self)
    if self.storage.allowLock then
        return error("this contract had beed opened allowLock before")
    end
    self.storage.allowLock = true
    emit AllowedLock("")
end

let function getBalanceOfUser(self: table, addr: string)
    return tointeger(self.storage.users[addr] or 0)
end

offline function M:balanceOf(owner: string)
    checkStateInited(self)
    if (not owner) or (#owner < 1) then
        return error('arg error, need owner address as argument')
    end
    checkAddress(owner)
    let amount = getBalanceOfUser(self, owner)
    let amountStr = tostring(amount)
    return amountStr
end

-- arg: limit(1-based),offset(0-based)}
offline function M:users(arg: string)
    let parsed = parse_args(arg, 2, "argument format error, need format is limit(1-based),offset(0-based)}")
    let info = {limit: tointeger(parsed[1]), offset: parsed[2]}
    let limit = tointeger(info.limit)
    let offset = tointeger(info.offset)
    if (not limit) or (limit < 1) or (not offset) or (offset <0) or ((offset + limit) <= 0) then
        return error("offset is non-negative integer, limit is positive integer")
    end
    let userAddresses: Array<string> = []
    var userAddr: string
    for userAddr in pairs(self.storage.users) do 
        table.append(userAddresses, userAddr)
    end
    var result: Array<string> = []
    if (#userAddresses <= offset) then
        result = []
    else
        var i: int = 0
        for i=offset,(offset+limit-1),1 do
            if i<#userAddresses then
                table.append(result, userAddresses[i+1])
            end
        end
    end
    let resultStr = tojsonstring(result)
    return resultStr
end

-- arg: to_address,integer_amount[,memo]
function M:transfer(arg: string)
    checkState(self)
    let parsed = parse_at_least_args(arg, 2, "argument format error, need format is to_address,integer_amount[,memo]")
    let info = {to: parsed[1], amount: tointeger(parsed[2])}
    let to = tostring(info.to)
    let amount = tointeger(info.amount)
    if (not to) or (#to < 1) then
        return error("to address format error")
    end
    if (not amount) or (amount < 1) then
        return error("amount format error")
    end
    checkAddress(to)
    let users = self.storage.users
    let from_address = get_from_address()
    if (not users[from_address]) or (users[from_address] < amount) then
        return error("you have not enoungh amount to transfer out")
    end
    if is_valid_contract_address(to) then
        let multiOwnedContract = import_contract_from_address(to)
        let amountStr = tostring(amount)
        if multiOwnedContract and (multiOwnedContract.on_deposit_contract_token) then
            multiOwnedContract:on_deposit_contract_token(amountStr)
        end
    end
    users[from_address] = tointeger(users[from_address] or 0) - amount
    if users[from_address] == 0 then
        users[from_address] = nil
    end
    users[to] = tointeger(users[to] or 0) + amount
    self.storage.users = users
    let eventArgStr = json.dumps({from: from_address, to: to, amount: amount})
    emit Transfer(eventArgStr)
end

-- arg format: fromAddress,toAddress,amount(with precision)
function M:transferFrom(arg: string)
    checkState(self)
    let parsed = parse_at_least_args(arg, 3, "argument format error, need format is fromAddress,toAddress,amount(with precision)")
    let fromAddress = tostring(parsed[1])
    let toAddress = tostring(parsed[2])
    let amount = tointeger(parsed[3])
    checkAddress(fromAddress)
    checkAddress(toAddress)
    if (not amount) or (amount < 0) then
        return error("amount must be positive integer")
    end
    let allowed = self.storage.allowed
    let users = self.storage.users
    if (not users[fromAddress]) or (amount > users[fromAddress]) then
        return error("fromAddress not have enough token to withdraw")
    end
    let allowedDataStr = allowed[fromAddress]
    if (not allowedDataStr) then
        return error("not enough approved amount to withdraw")
    end
    let allowedData: Map<int> = totable(json.loads(allowedDataStr))
    let contractCaller = get_from_address()
    if (not allowedData) or (not allowedData[contractCaller]) then
        return error("not enough approved amount to withdraw")
    end
    let approvedAmount = tointeger(allowedData[contractCaller])
    if (not approvedAmount) or (amount > approvedAmount) then
        return error("not enough approved amount to withdraw")
    end
    users[toAddress] = tointeger(users[toAddress] or 0) + amount
    users[fromAddress] = users[fromAddress] - amount
    if users[fromAddress] == 0 then
        users[fromAddress] = nil
    end
    allowedData[contractCaller] = approvedAmount - amount
    if allowedData[contractCaller] == 0 then
        allowedData[contractCaller] = nil
    end
    allowed[fromAddress] = json.dumps(allowedData)
    self.storage.users = users
    self.storage.allowed = allowed
    let eventArgStr = json.dumps({from: fromAddress, to: toAddress, amount: amount})
    emit Transfer(eventArgStr)
end

-- arg format: spenderAddress,amount(with precision)
function M:approve(arg: string)
    checkState(self)
    let allowed = self.storage.allowed
    let parsed = parse_at_least_args(arg, 2, "argument format error, need format is spenderAddress,amount(with precision)")
    let spender = tostring(parsed[1])
    checkAddress(spender)
    let amount = tointeger(parsed[2])
    if (not amount) or (amount < 0) then
        return error("amount must be non-negative integer")
    end
    var allowedData: Map<int>
    let contractCaller = get_from_address()
    if (not allowed[contractCaller]) then
        allowedData = {}
    else
        allowedData = totable(json.loads(allowed[contractCaller]))
        if allowedData then
            return error("allowed storage data error")
        end
    end
    allowedData[spender] = amount
    allowed[contractCaller] = json.dumps(allowedData)
    self.storage.allowed = allowed
    let eventArg = {from: contractCaller, spender: spender, amount: amount}
    emit Approved(json.dumps(eventArg))
end

-- arg format: spenderAddress,authorizerAddress
offline function M:approvedBalanceFrom(arg: string)
    let allowed = self.storage.allowed
    let parsed = parse_at_least_args(arg, 2, "argument format error, need format is spenderAddress,authorizerAddress")
    let spender = tostring(parsed[1])
    let authorizer = tostring(parsed[2])
    checkAddress(spender)
    checkAddress(authorizer)
    let allowedDataStr = allowed[authorizer]
    if (not allowedDataStr) then
        return "0"
    end
    let allowedData: Map<int> = totable(json.loads(allowedDataStr))
    if (not allowedData) then
        return "0"
    end
    let allowedAmount = allowedData[spender]
    if (not allowedAmount) then
        return "0"
    end
    let allowedAmountStr = tostring(allowedAmount)
    return allowedAmountStr
end

-- arg format: fromAddress
offline function M:allApprovedFromUser(arg: string)
    let allowed = self.storage.allowed
    let authorizer = arg
    checkAddress(authorizer)
    let allowedDataStr = allowed[authorizer]
    if (not allowedDataStr) then
        return "{}"
    end
    return allowedDataStr
end

function M:pause(arg: string)
    if self.storage.state == 'STOPPED' then
        return error("this contract stopped now, can't pause")
    end
    if self.storage.state == 'PAUSED' then
        return error("this contract paused now, can't pause")
    end
    checkAdmin(self)
    self.storage.state = 'PAUSED'
    emit Paused("")
end

function M:resume(arg: string)
    if self.storage.state ~= 'PAUSED' then
        return error("this contract not paused now, can't resume")
    end
    checkAdmin(self)
    self.storage.state = 'COMMON'
    emit Resumed("")
end

function M:stop(arg: string)
    if self.storage.state == 'STOPPED' then
        return error("this contract stopped now, can't stop")
    end
    if self.storage.state == 'PAUSED' then
        return error("this contract paused now, can't stop")
    end
    checkAdmin(self)
    self.storage.state = 'STOPPED'
    emit Stopped("")
end

-- arg: integer_amount,unlockBlockNumber
function M:lock(arg: string)
    checkState(self)
    if (not self.storage.allowLock) then
        return error("this token contract not allow lock balance")
    end
    let parsed = parse_args(arg, 2, "arg format error, need format is integer_amount,unlockBlockNumber")
    let toLockAmount = tointeger(parsed[1])
    let unlockBlockNumber = tointeger(parsed[2])
    if (not toLockAmount) or (toLockAmount<1) then
        return error("to unlock amount must be positive integer")
    end
    if (not unlockBlockNumber) or (unlockBlockNumber < get_header_block_num()) then
        return error("to unlock block number can't be earlier than current block number " .. tostring(get_header_block_num()))
    end
    let from_address = get_from_address()
    if from_address ~= caller_address then
        return error("only common user account can lock balance")
    end
    let balance = getBalanceOfUser(self, from_address)
    if (toLockAmount > balance) then
        return error("you have not enough balance to lock")
    end
    let lockedAmounts = self.storage.lockedAmounts
    if (not lockedAmounts[from_address]) then
        lockedAmounts[from_address] = tostring(toLockAmount) .. ',' .. tostring(unlockBlockNumber)
    else
        return error("you have locked balance now, before lock again, you need unlock them or use other address to lock")
    end
    self.storage.lockedAmounts = lockedAmounts
    self.storage.users[from_address] = balance - toLockAmount
    emit Locked(tostring(toLockAmount))
end

function M:unlock(_: string)
    checkState(self)
    if (not self.storage.allowLock) then
        return error("this token contract not allow lock balance")
    end
    let from_address = get_from_address()
    let lockedAmounts = self.storage.lockedAmounts
    if (not lockedAmounts[from_address]) then
        return error("you have not locked balance")
    end
    let lockedInfoParsed = parse_args(lockedAmounts[from_address], 2, "locked amount info format error")
    let lockedAmount = tointeger(lockedInfoParsed[1])
    let canUnlockBlockNumber = tointeger(lockedInfoParsed[2])

    if (get_header_block_num() < canUnlockBlockNumber) then
        return error("your locked balance only can be unlock after block #" .. tostring(canUnlockBlockNumber))
    end
    lockedAmounts[from_address] = nil
    self.storage.lockedAmounts = lockedAmounts
    self.storage.users[from_address] = getBalanceOfUser(self, from_address) + lockedAmount
    emit Unlocked(from_address .. ',' .. tostring(lockedAmount))
end

-- arg: userAddress
-- only admin can call this api
function M:forceUnlock(arg: string)
    checkState(self)
    if (not self.storage.allowLock) then
        return error("this token contract not allow lock balance")
    end
    checkAdmin(self)
    let userAddr = arg
    if (not userAddr) or (#userAddr < 1) then
        return error("argument format error, need format userAddress")
    end
    checkAddress(userAddr)

    let lockedAmounts = self.storage.lockedAmounts
    if (not lockedAmounts[userAddr]) then
        return error("this user have not locked balance")
    end
    let lockedInfoParsed = parse_args(lockedAmounts[userAddr], 2, "locked amount info format error")
    let lockedAmount = tointeger(lockedInfoParsed[1])
    let canUnlockBlockNumber = tointeger(lockedInfoParsed[2])

    if (get_header_block_num() < canUnlockBlockNumber) then
        return error("this user locked balance only can be unlock after block #" .. tostring(canUnlockBlockNumber))
    end
    lockedAmounts[userAddr] = nil
    self.storage.lockedAmounts = lockedAmounts
    self.storage.users[userAddr] = getBalanceOfUser(self, userAddr) + lockedAmount
    emit Unlocked(userAddr .. ',' .. tostring(lockedAmount))
end

offline function M:lockedBalanceOf(owner: string)
    let lockedAmounts = self.storage.lockedAmounts
    if (not lockedAmounts[owner]) then
        return '0,0'
    else
        let resultStr = lockedAmounts[owner]
        return resultStr
    end
end

function M:on_destroy()
    error("can't destroy token contract")
end

return M

⚠️ **GitHub.com Fallback** ⚠️