Exclusive Claimable Token
ERC-3135, titled "Exclusive Claimable Token," proposes a token that can be claimed only by the token issuer with the payer's signature. This standard aims to establish micropayment channels between service providers and any number of users by signing and verifying messages about token consumption off-chain. This mechanism reduces interactions with the blockchain, saving gas and improving performance.
The standard introduces several new functions, including withdraw, depositBalanceOf, and events like Deposit, Withdraw, TransferIssuer, and Claim. The withdraw function can be executed differently based on business models, such as the prepayment model and the lock-release model.
In the prepayment model, the user pays (crypto)currency to the store in advance for claimable tokens. When checking out, the customer signs a message with updated consumption to the store, and the store verifies this message off-chain. In the lock-release model, the counting of services happens on both client and server side. The client should not trust the charge amount given by the server in case it's malfunctioning or malicious.
The standard also discusses the signature generation process, the verification process, and security considerations, including the potential for double-spending problems.
Video
Original
Simple Summary
This standard defines a token which can be claimed only by token issuer with payer's signature.
Abstract
This EIP defines a set of additions to the default token standard such as ERC-20, that allows online/offline service providers establish micropayment channels with any number of users by signing and verifying messages about the consumption of token off chain. Using this mechanism will reduce interactions with blockchain to minimal for both participants, thus saving gas and improve performance.
Motivation
There are two main purposes of this EIP, one is to reduce interactions with blockchain, the second is to link Ethereum to real-world payment problems.
Many small businesses want to build payment system based on blockchain but find it difficult. There are basically two ways:
- Directly pay with token. There are many wallet can receive and transfer token but transactions on Ethereum cost gas and take time to confirm.
- User lock token on payment smart contract and service provider use payment messages signed by user to release token, establishing a micropayment channel. The advantage is interactions with blockchain is reduced and the signing/verifying process is off-chain. But interact with payment contract needs service provider to build a DApp, which require resources many small businesses do not have. Even if they managed to build DApps, they are all different, not standardized. Also, user should have a wallet with DApp browser and has to learn how to use it.
This EIP helps to standardize the interactions of micropayment system, and make it possible for wallet build a universal UI in the future.
Specification
/// @return Image url of this token or descriptive resources function iconUrl() external view returns (string memory); /// @return Issuer of this token. Only issuer can execute claim function function issuer() external view returns (address); /** * @notice Remove consumption from payer's deposite * @dev Check if msg.sender == issuer * @param from Payer's address * @param consumption How many token is consumed in this epoch, specified * @param epoch Epoch increased by 1 after claim or withdraw, at the beginning of each epoch, consumption goes back to 0 * @param signature Signature of payment message signed by payer */ function claim(address from, uint256 consumption, uint256 epoch, bytes calldata signature) external; function transferIssuer(address newIssuer) external; /// @notice Move amount from payer's token balance to deposite balance to ensure payment is sufficient function deposit(uint256 amount) external; /** * @notice Give remaining deposite balance back to "to" account, act as "refund" function * @dev In prepayment module, withdraw is executed from issuer account * In lock-release module, withdraw is executed from user account * @param to the account receiving remaining deposite * @param amount how many token is returned */ function withdraw(address to, uint256 amount) external; function depositBalanceOf(address user) external view returns(uint256 depositBalance, uint256 epoch); event Deposit( address indexed from, uint256 amount ); event Withdraw( address indexed to, uint256 amount ); event TransferIssuer( address indexed oldIssuer, address indexed newIssuer ); event Claim( address indexed from, address indexed to, uint256 epoch, uint256 consumption );
signature
the pseudo code generating an ECDSA signature:
sign(keccak256(abi_encode(
"\x19Ethereum Signed Message:\n32",
keccak256(abi_encode(
token_address,
payer_address,
token_issuer,
token_consumption, //calculated by user client
epoch
))
))
,private_key)
verification process
the verification contains check about both signature and token_consumption
the pseudo code run by verification server is as follows:
serving_loop:
for {
/**
* unpaied_consumption is calculated by provider
* signed_consumption is claimable amount
* tolerance allows payer "owes" provider to a certain degree
*/
//getSignedConsumption returns amount that are already claimable
if(unpaied_consumption < signed_consumption + tolerance){
informUser("user need charge", unpaied_consumption)
interruptService()
}else{
isServing() || recoverService()
}
}
verification_loop:
for {
message = incomingMessage()
if(recover_signer(message, signature) != payer_address){
informUser("check signature failed", hash(message))
continue
}
/**
* optional: when using echo server to sync messages between verification servers
* more info about this in Security Considerations section
*/
if(query(message) != message){
informUser("message outdate", hash(message))
continue
}
if(epoch != message.epoch || message.consumption > getDepositBalance()){
informUser("invalid message", epoch, unpaied_consumption)
continue
}
signed_consumption = message.consumption
save(message)
}
claim_process:
if(claim()){
unpaied_consumption -= signed_consumption
signed_consumption = 0
epoch+=1
}
About withdraw
The withdraw function is slightly different based on business models
- prepayment model
In prepayment business model such as using token as recharge card of general store, the user pays (crypto)currency to store in advance for claimable token as recharge card (with bonus or discount). When checking out, the customer signs a message with updated consumption (old consumption + consumption this time) to store and store verifies this message off chain. The shopping process loops without any blockchain involved, until the customer wants to return the card and get money back. Because the store already holds all currency, the withdraw function should be executed by token issuer (store) to return remaining deposit balance after claim. The prepayment model can easily be built into a wallet with QR-code scanning function.
- lock-release model
If we run a paid end-to-end encrypted e-mail service that accepts token as payment, we can use lock-release model. Unlike prepayment, we charge X * N token for an e-mail sent to N recipients. In this "pay for usage" scenario, the counting of services happens on both client and server side. The client should not trust charge amount given by server in case the it's malfunctioning or malicious. When client decide not to trust server, it stops signing messages, but some of token is taken hostage in deposit balance. To fix this problem, the withdraw function should be executed by payer account with limitation such as epoch didn't change in a month.
Rationale
This EIP targets on ERC-20 tokens due to its widespread adoption. However, this extension is designed to be compatible with other token standard.
The reason we chose to implement those functions in token contract rather than a separate record contract is as follows:
- Token can transfer is more convenient and more general than interact with DApp
- Token is more standardized and has better UI support
- Token is equal to service, make token economy more prosperous
- Remove the approve process
Backwards Compatibility
This EIP is fully backwards compatible as its implementation extends the functionality of ERC-20.
Implementation
mapping (address => StampBalance) private _depositBalance; struct StampBalance{ uint256 balance; uint256 epoch; } function deposit(uint256 value) override external{ require(value <= _balances[msg.sender]); _balances[msg.sender] = _balances[msg.sender].sub(value); _depositBalance[msg.sender].balance = _depositBalance[msg.sender].balance.add(value); emit Deposit(msg.sender, value); } function withdraw(address to, uint256 value) override onlyIssuer external{ require(value <= _depositBalance[to].balance); _depositBalance[to].balance = _depositBalance[to].balance.sub(value); _depositBalance[to].epoch += 1; _balances[to] = _balances[to].add(value); emit Withdraw(to, value); } function depositBalanceOf(address user) override public view returns(uint256 depositBalance, uint256 epoch){ return (_depositBalance[user].balance, _depositBalance[user].epoch); } // prepayment model function claim(address from, uint credit, uint epoch, bytes memory signature) override onlyIssuer external{ require(credit > 0); require(_depositBalance[from].epoch + 1 == epoch); require(_depositBalance[from].balance >= credit); bytes32 message = keccak256(abi.encode(this, from, _issuer, credit, epoch)); bytes32 msgHash = prefixed(message); require(recoverSigner(msgHash, signature) == from); _depositBalance[from].balance = _depositBalance[from].balance.sub(credit); _balances[_issuer] = _balances[_issuer].add(credit); _depositBalance[from].epoch += 1; emit Claim(from, msg.sender, credit, epoch); } function prefixed(bytes32 hash) internal pure returns (bytes32) { return keccak256(abi.encode("\x19Ethereum Signed Message:\n32", hash)); } function recoverSigner(bytes32 message, bytes memory sig) internal pure returns (address) { (uint8 v, bytes32 r, bytes32 s) = splitSignature(sig); return ecrecover(message, v, r, s); } function splitSignature(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) { require(sig.length == 65); assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } return (v, r, s); }
Security Considerations
By restricting claim function to issuer, there is no race condition on chain layer. However double spending problem may occur when the issuer use multiple verifiers and payer signs many payment messages simultaneously. Some of those messages may get chance to be checked valid though only the message with the largest consumption can be claimed. This problem can be fixed by introducing an echo server which accepts messages from verifiers, returns the message sequentially with largest consumption and biggest epoch number. If a verifier gets an answer different from the message he send, it updates the message from echo server as the last message it receives along with local storage of the status about this payer. Then the verifier asks the payer again for a new message.
Copyright
Copyright and related rights waived via CC0.
Adopted by projects
Not miss a beat of EIPs' update?
Subscribe EIPs Fun to receive the latest updates of EIPs Good for Buidlers to follow up.
View all