ERC-7291 Purpose Bound Money (PBM) (Purpose bound money)
Final · Tokens · created 2023-06-24
A standard that wraps digital money such as stablecoins in an ERC-1155 wrapper to create "purpose-bound money" that is released as the original currency only when set conditions are met and it is delivered to a designated merchant.
Why it is needed
To make digital versions of "money that can only be spent at designated places," such as government vouchers or education subsidies, you need both programmable money, which embeds usage conditions in the money itself, and programmable payments, which pay out automatically when conditions are met. Without a standard, each issuer implements it differently, which fragments the technology and breaks compatibility between wallets and payment systems, creating closed ecosystems (walled gardens). New entrants find it hard to integrate with existing systems, so costs rise and users end up with expensive and inconvenient services. This standard aims to make the PBM specification an open standard, securing interoperability among platforms, wallets, and payment rails and lowering the barrier to entry.
What to implement
function initialise(address _sovToken, uint256 _expiry, address _pbmWrapperLogic) external;: Initializes the PBM wrapper by specifying the underlying currency (sovToken) address, the contract expiry time, and the address of the PBM condition logic contract.function getTokenDetails(uint256 tokenId) external view returns(PBMToken memory pbmToken_);: Queries the details (PBMToken struct) of the PBM token type that corresponds to the tokenId.function safeMint(address receiver, uint256 tokenId, uint256 amount, bytes calldata data) external;: Mints a copy of an existing PBM token type and sends it to the recipient. It pulls the caller's sovToken into the PBM contract and wraps it, and MUST emit the TokenWrap event.function safeMintBatch(address receiver, uint256[] calldata tokenIds, uint256[] calldata amounts, bytes calldata data) external;: Mints multiple PBM token types at once. It wraps sovToken in the same way as safeMint.function burn(address from, uint256 tokenId, uint256 amount, bytes calldata data) external;: Burns PBM tokens. When the wrapped underlying token moves out, it MUST emit the TokenUnwrapForPBMBurn event.function burnBatch(address from, uint256[] calldata tokenIds, uint256[] calldata amounts, bytes calldata data) external;: Burns multiple PBM tokens at once.function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) external;: Transfers a PBM to another wallet. Transfer condition checks and unwrapping logic can be placed inside this function.function safeBatchTransferFrom(address from, address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external;: Transfers multiple PBMs at once. If the recipient is a merchant on the whitelist, it burns the PBM and delivers the underlying sovToken instead.function unwrap(address from, address to, uint256 tokenId, uint256 amount, bytes memory data) internal;: An internal function that releases the underlying token and delivers it to the designated recipient when the PBM conditions are met. On success, it MUST emit the TokenUnwrapForTarget event.function revokePBM(uint256 tokenId) external;: The creator of the PBM token type (or the contract owner) reclaims the sovToken locked in that PBM. It MUST emit the PBMrevokeWithdraw event.function isBlacklisted(address _address) external returns (bool bool_) ;: Checks whether an address is on the blocklist (blacklist).function isMerchant(address _address) external returns (bool bool_) ;: Checks whether an address is on the merchant and recipient whitelist and not on the blacklist.event NewPBMTypeCreated(uint256 tokenId, string tokenName, uint256 amount, uint256 expiry, address creator);: Emitted when a new PBM token type is created.event TokenWrap(address from, uint256[] tokenIds, uint256[] amounts,address sovToken, uint256 sovTokenValue);: Emitted when the underlying token is wrapped in the PBM contract and the PBM lifecycle begins.event TokenUnwrapForTarget(address from, address to, uint256[] tokenIds, uint256[] amounts, address sovToken, uint256 sovTokenValue);: Emitted when all conditions are met and the underlying token is released and delivered to its destination (a whitelisted merchant).event TokenUnwrapForPBMBurn(address from, address to, uint256[] tokenIds, uint256[] amounts, address sovToken, uint256 sovTokenValue);: Emitted when the underlying token is unwrapped during PBM burning.event PBMrevokeWithdraw(address beneficiary, uint256 PBMTokenId, address sovToken, uint256 sovTokenValue);: Emitted when the creator reclaims the underlying sovToken remaining in an expired PBM.function load(uint256 amount) external;: (PBMRC2 extension) Fills an empty PBM with sovToken later. Emits the TokenLoad event.function underlyingBalanceOf(address user) external view returns (uint256);: (PBMRC2 extension) Queries the underlying sovToken balance that a user has loaded into a PBM.function unload(uint256 amount) external;: (PBMRC2 extension) The caller withdraws the underlying token they loaded earlier and gets it back. Emits the TokenUnload event.function onPBMRC1Unwrap(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) external returns(bytes4);: A callback through which the receiving contract is notified of a PBM unwrap. It is called at the end of unwrap.
In depth
PBM structure: sovToken and PBM wrapper
A valid PBM token MUST consist of an underlying sovToken and a PBM wrapper. The sovToken is an ERC-20 or ERC-20 compatible token (such as ERC-777 or ERC-1363) and serves as the store of value. The PBM wrapper is an ERC-1155 compatible contract that holds the conditions that must be met before unwrapping. Wrapping can happen when the PBM is created or later. A PBM token that has been fully unwrapped and used MUST be burned, and the expiry time is set by the PBM creator, but if no expiry is needed, setting it to type(uint256).max is RECOMMENDED (SHOULD). A single PBM wrapper SHOULD be able to wrap multiple kinds of sovToken.
Whitelist and blacklist verification
Before unwrapping the underlying sovToken, the PBM wrapper MUST check that the destination address is on the merchant/recipient whitelist and not on the blocking blacklist. The whitelist is a required element for a PBM to be purpose-bound, and the blacklist is needed to exclude addresses blocked for reasons such as account re-registration, suspension, or law enforcement. Using only a whitelist would require redeploying the entire list each time one party is blocked, which is inefficient in gas cost, and using only a blacklist would require managing every address to be excluded, which is impractical, so both are required. This verification can be implemented inside the PBM wrapper or separated into a standalone compliance guard contract.
Modular architecture: core, plugins, hooks
The proposed PBM architecture is divided into three parts. The core component contains the basic functions every PBM must have: merchant whitelist logic, unwrapping on transfer to a whitelisted address, minting and burning, and a token manager that handles token registration and lookup. The plugin component implements additional functions that only specific PBMs need, such as verifying conditions through external API calls and tracking usage patterns. The hook component implements additional verification that applies to only some PBMs, such as expiry checks and daily spending limits. For example, reusability can be improved by using the same wrapper and sovToken while registering a plugin and hook for a "50% supermarket spending limit" only on Series A.
Token details and the PBMToken struct
Each ERC-1155 token ID defines a distinct PBM type, and each type MUST have the required fields name (the name), faceValue (the face value in terms of the underlying sovToken), expiry (the expiry as a Unix timestamp), and uri (the metadata URI for ERC-1155 display). Once defined, token details MUST be immutable. The optional extension struct PBMTokenExt may include fields such as isTransferable, burnable, decimals, creator, tokenAddress, totalSupply, currencySymbol, and tokenSymbol. A NewPBMTypeCreated event SHOULD be emitted when a token type is created, and a method to look up the details with getTokenDetails MUST be provided.
Lifecycle of the base interface IPBMRC1
IPBMRC1 inherits IERC173 (ownership) and IERC5679Ext1155 (minting and burning). safeMint and safeMintBatch pull the caller's sovToken into the contract and wrap it, require a prior ERC-20 approve, and MUST emit a TokenWrap event. In addition, the contract must not be paused, the token must not be expired, and the recipient must not be on the blacklist. The internal function unwrap delivers the underlying token to the merchant when the conditions are met and MUST emit a TokenUnwrapForTarget event. revokePBM can be called only by the token type creator or the contract owner, reclaims the locked sovToken, and MUST emit a PBMrevokeWithdraw event. Transfers follow a push model initiated by the sender, and unwrapping logic can be embedded in the ERC-1155 safeTransfer family of functions.
Extensions: receiver callbacks and non-preloaded PBM (PBMRC2)
A receiving contract that wants to be notified of unwrapping MUST implement both onPBMRC1Unwrap and onPBMRC1BatchUnwrap from the PBMRC1_TokenReceiver interface and signal support through ERC-165 supportsInterface. These callbacks MUST be invoked only at the very end of the unwrap function. The optional extension PBMRC2 (Non Preloaded PBM) fills in the sovToken later rather than at minting time. The PBM is first minted as an empty container, then sovToken is put in with load (TokenLoad event), the balance is queried with underlyingBalanceOf, and the holder can take back the amount they put in with unload (TokenUnload event).
Why sovToken is ERC-20 and the wrapper is ERC-1155
A PBM must function as money, so it needs a fungible token with stable value. Because the major stablecoins on the market are ERC-20 based and wallet and DeFi app support is broadest for it, the sovToken must be ERC-20 compatible. ERC-721 is a non-fungible token and does not fit the function of money, and ERC-1155 stablecoins are rare on the market, so they were excluded. The wrapper is ERC-1155 because a single contract can issue multiple tokens with different face values, expiries, and business logic, batch transfers save gas on large payouts, semi-fungible tokens can be converted into collectibles after expiry, and existing wallet UIs display them directly.
Things to watch out for
- Double spending and forgery attempts: A malicious user may attempt double spending through reentrancy, duplicate an existing PBM token, create an invalid PBM with no underlying sovToken, or wrap a fake or worthless sovToken to misrepresent the face value. Security audits and tests of the unwrapping logic are needed.
- Consistency of the paused state: If the contract is paused or a user's token transfers are restricted because of suspected fraud or a mistaken transfer, the same restriction MUST be applied to that user's PBM unwrapping requests.
- Managing minting authority: Failing to properly control the authority to mint new tokens can lead to fraudulent minting and unauthorized supply inflation. The mapping between each PBM token and the amount of sovToken held by the contract requires careful accounting and auditing.
- Designing burn authority: In general, before expiry only whitelisted merchants can burn, and after expiry only merchants and the PBM owner (to reclaim unused amounts) can burn. When burning is allowed for a third role, access control must be designed carefully.
- Choosing the sovToken standard: Adopting an ERC-20 compatible standard (including ERC-777 and ERC-1363) is RECOMMENDED, and among them ERC-20 is the most widely accepted thanks to its simplicity and proven security.