onKeyPurchaseHook is called when a purchase is triggered and can be used to customize your purchase workflow.
There are 2 already available contract extensions that allow to use the
onKeyPurchaseHook hook, namely:
CodeRequiredHook: require a secret code to successfuly purchase memberships
DiscountCodeHook: add discount codes to your lock
Code Required Hook
CodeRequiredHook contract allows lock creators to restrict key purchases.
Simply put, with this feature we can require a secret is known in order to purchase keys. Here are a few use cases this enables:
Enter 'this is a beta' to gain access
You might want to limit access to a smaller group, e.g. maybe you are beta testing a new feature and want to invite everyone from your Telegram channel. You could offer a key for free, but require they explicitly acknowledge the risks by entering this code.
Obviously someone could Tweet the code, spreading it future than intended. However it still effectively limits access to "insiders".
A whitelist can be created with this feature as well. This allows a trusted operator to confirm which ETH accounts may purchase a key. e.g. maybe you need to check IDs first.
Once approved, the operator shares the signature required to purchase. This allows only that account to purchase and the secret itself can remain secure (with the operator).
The CodeRequiredHook requires knowing a secret. The secret can be anything (most commonly a string) but then it's converted to a private key (and we have recommendations for that process).
Given a private key, we have a public key as well - an Ethereum address (with 0 funds). We call this the
codeAccount. We can store the
codeAccount.address publicly without risking exposing the secret itself on-chain.
In order to purchase, we generate a signature with
keyOwner is the address of the account that's approved to purchase a key. Including the
keyOwner ensures that another account cannot simply replay the same transaction to purchase for themselves.
When issuing a transaction to purchase, the signature created by the
codeAccount is included in the
bytes _data field. The lock forwards this information to the CodeRequiredHook for confirmation. If the signature is missing or not correct then the entire transaction reverts.
Discount Codes Hook
We created the
DiscountCodeHook contract that allows you to add discount codes to your lock. For example, enter the code
discount50 for 50% off.
The contract is deployed here and may be leveraged by all v7+ locks (i.e. you do not need to deploy your own discount code contract):
- Mainnet address: 0x3c895c794be4dc403f8802ef6d95aa1de3cbb04c
- Rinkeby address: 0x13738ae895f1e35eedf7f3227109567a5ce40eab
How it works
A discount code is simply a secret. The secret can be anything but is most commonly a string.
The code is hashed to create a private key. Given a private key, we can generate an Ethereum address. We call this the
codeAccount and we can store the
codeAddress) publicly without exposing the secret itself on-chain.
In order to purchase with a discount applied, we generate a signature with
keyOwner is the address of the account that's purchasing a key.
When issuing the purchase transaction, the signature is included in the
bytes _data field. The lock forwards this information to the discount code contract for confirmation. If the signature is missing or incorrect then the user is charged full price.
There are a couple steps required in order to start offering discount codes on your lock.
Register the hook
The discount code contract is written as a
keyPurchaseHook. The lock manager needs to configure the lock's hook to point to the discount code contract. By default there are none set in order to remain flexible so lock managers can choose which features they would like to add to their lock.
To register, call the following function with the discount code address above:
address _onKeyPurchaseHook, // the deployed DiscountCodeHook contract address
You can set the others hooks to to
0x0000000000000000000000000000000000000000 if you don't have a use case for that. Additional you can de-register the hook anytime by setting both params to 0.
Add discount codes
Once the discount code contract has been registered you can start adding codes.
First we need to generate the
codeAddress. This step is required so that we don't publish the code directly as all information on Ethereum is public and we don't want users to simply look at the transaction history in order to discover a discount that anyone could use.
To generate the
codeAddress the following steps are recommended. This process is not yet integrated onto our dashboard so they will need to be performed manually. Examples below are in javascipt using web3.js.
Sanitize the input for ease of use. For example, if the code is "discount50" we want to be flexible on how that is written by the end user: "discount50" and "Discount50" should also be accepted. We recommend you remove whitespace (and maybe underscores as well) and lower case the input before proceeding to the next step.
Generate the private key from the sanitized input. Include your lock's address so that the same discount code may be used by multiple different locks without making it easy for people to discover that the same code works elsewhere. For this we recommend the following:
const codePrivateKey = web3.utils.keccak256(
Generate the account representing the discount code with the following:
const codeAccount = web3.eth.accounts.privateKeyToAccount(codePrivateKey);
Get the address for the
codeAccountwhich is what we will store on-chain:
const codeAddress = codeAccount.address;
addCodeson the discount code contract from the lock manager's account. You can use Etherscan's Write Contract page to make the call. There are 3 parameters:
_lockis the address of the lock that this discount applies to. You can copy this from the Unlock dashboard.
_codeAddressesis an array of addresses. If you are only adding a single discount code then it can be populated like so:
_discountBasisPointsis also an array. The discounts are represented in basis points which means 100 represents 1%. This allows you to be more precise on discounts, e.g. if you wanted to offer 12.42% off. The order should align with the
_codeAddressesso that if the first discount code is 15% off and the second is 50% off you can enter the following:
To remove a code in the future, just call
addCodes again and set the discount for that
codeAddress to 0.
Discount codes have not yet been integrated into the frontend provided by Unlock-Protocol. However anyone can make their own frontend experience.
In order to add support for discount codes, there are a few steps required:
Add an input box allowing end users to enter a discount code if they have one.
Sanitize the input and generate the
codeAccountusing the same approach that we used when adding the discount code originally (steps 1-3).
Generate the message to sign by hashing the address of the account purchasing a key as follows:
const messageToSign = web3.utils.keccak256(
Then sign that message:
const signature = (await codeAccount.sign(messageToSign)).signature;
This function will sign the message as follows
"\x19Ethereum Signed Message:\n32" + messageToSign.
To confirm the discount code and to display the correct price for this purchase, use the following function:
const purchasePrice = await lock.purchasePriceFor(
"0x0000000000000000000000000000000000000000", // Referrer, n/a here
purchasecall to use the
purchasePricecalculated above and include the signature if the user has entered a discount code. For example:
"0x0000000000000000000000000000000000000000", // Referrer, n/a here
If the code is missing or incorrect, the lock's normal
keyPrice will be used.
How secure this solution is depends on the discount code you select. For common use cases such as the
discount50 example we have been working with, it would not be difficult for someone to guess and check to find that discount.
If security is a concern, you could generate discount codes that look a bit more like an Amazon gift card as they have enough entropy that users would not be able to brute force it without incurring significant cost.
Even if the code you select has enough randomness to it, anyone who does know the code could post it publicly for others to use as well.
keyOwner is included in the signature to ensure that another account cannot simply replay the same transaction
_data field to purchase with a discount for themselves.
Some of the process described above is our recommendations. The contract itself will not enforce these best practices if you choose to use a different strategy, specifically:
- Sanitizing the input reduces entropy but makes for a better user experience.
- How the
codeAccountitself is generated is flexible, for example including the lock's address in the private key prevents users from discovering that a discount code can also be used on another lock.
If you have a requirement for discount codes which is not addressed here (e.g. single use codes), reach out and let's discuss.