BOBA Faucet

Turing Example - CAPTCHA-based token faucet
Boba Faucet is a system for distributing Rinkeby ETH and Rinkeby BOBA. It's implemented using Turing hybrid compute. Before claiming tokens, users answer a CAPTCHA. Their answer is hashed and compared off-chain to the correct answer via Turing. Once their answer is verified, the smart contract releases the funds.

Directory Structure

  • boba_community/turing-captcha-faucet/packages: Contains all the typescript packages and contracts
    • contracts: Smart contracts implementing the Boba Faucet
    • gateway: The Boba Web faucet frontend
    • deployment: Boba faucet Rinkeby contract addresses
    • api: Boba faucet backend API


The token-claiming process takes place in five steps:

1. User obtains the CAPTCHA image and the image UUID

The API GET request is sent to on the frontend. The returned payload is
"UUID": "BYTES32",
"imageBase64": "CAPTCHA IMAGE"
The UUID and hashed CAPTCHA answer are stored in AWS Redis.

2. User sends a transaction to the Boba Faucet contract with the UUID and CAPTCHA answer

const BobaFaucet = new ethers.Contract(
const tx = await BobaFaucet.getBobaFaucet(uuid, answer)
await tx.wait()
The answer is hashed in the Boba Faucet contract first before sending it to backend API.
bytes32 hashedKey = keccak256(abi.encodePacked(_key));
bytes memory encRequest = abi.encodePacked(_uuid, hashedKey);
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);

3. Geth sends a request to the backend and retrieves the result

The POST request with the hashed answer is sent to It decodes the input and verifies the UUID with the hashed answer.
paramsHexString = body['params'][0]
# Select uuid and answer
# paramsHexString[0: 66] is the length of input data
uuid = '0x' + paramsHexString[66: 130]
answer = '0x' + paramsHexString[130:]
# Get answer from Redis
keyInRedis = db.get(uuid)
# Return the payload
return returnPayload(keyInRedis.decode('utf-8') == key)
# Payload is built based on the result
def returnPayload(result):
# We return 0 or 1 using uint256
payload = '0x' + '{0:0{1}x}'.format(int(32), 64)
if result:
# Add UINT256 1 if the result is correct
payload += '{0:0{1}x}'.format(int(1), 64)
# Add UINT256 0 if the result is wrong
payload += '{0:0{1}x}'.format(int(0), 64)
returnPayload = {
'statusCode': 200,
'body': json.dumps({ 'result': payload })
return returnPayload

4. Geth atomically revises the calldata

On the contract level, we decode the result from the Turing request and release the funds if the answer is correct.
// Decode the response from outside API
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
uint256 result = abi.decode(encResponse,(uint256));
// Release the funds if it is correct
require(result == 1, 'Captcha wrong');
IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);

5. User obtains the funds if the answer is correct or sees an error message



Step 1: Creating API endpoints

Two simple API endpoints are created.
image = ImageCaptcha(width=280, height=90)
# For image
uuid1 = uuid.uuid4()
imageStr = str(uuid1).split('-')[0]
imageStrBytes = Web3.solidityKeccak(['string'], [str(imageStr)]).hex()
# For key
uuid2 = uuid.uuid4()
keyBytes = Web3.solidityKeccak(['string'], [str(uuid2)]).hex()
# Use the first random string
imageData = image.generate(imageStr)
# Get base64
imageBase64 = base64.b64encode(imageData.getvalue())
# Read YML
with open("env.yml", 'r') as ymlfile:
config = yaml.load(ymlfile)
# Connect to Redis Elasticache
db = redis.StrictRedis(host=config.get('REDIS_URL'),
# Store and set the expire time as 10 minutes
print('keyBytes: ', keyBytes, 'imageStrBytes: ', imageStrBytes, 'imageStr: ', imageStr)
db.set(keyBytes, imageStrBytes, ex=600)
payload = {'uuid': keyBytes, 'imageBase64': imageBase64.decode('utf-8') }
paramsHexString = body['params'][0]
uuid = '0x' + paramsHexString[66: 130]
key = '0x' + paramsHexString[130:]
# Read YML
with open("env.yml", 'r') as ymlfile:
config = yaml.load(ymlfile)
# Connect to Redis Elasticache
db = redis.StrictRedis(host=config.get('REDIS_URL'),
# Store and set the expire time as 10 minutes
keyInRedis = db.get(uuid)
if keyInRedis:
print('keyInRedis: ', keyInRedis.decode('utf-8'), 'key: ', key)
return returnPayload(keyInRedis.decode('utf-8') == key)
return returnPayload(False)

Step 2: Creating the Boba Faucet Contract

BOBA Faucet Smart Contracts


Create a .env file in the root directory of the contracts folder. Add environment-specific variables on new lines in the form of NAME=VALUE. Examples are given in the .env.example file. Just pick which net you want to work on and copy either the "Rinkeby" or the "Local" envs to your .env.
Build and deploy all the needed contracts:
$ yarn build
$ yarn deploy
The smart contract imports the Turing Helper Contract so it can interact with outside API endpoints.
import './TuringHelper.sol';
contract BobaFaucet is Ownable {
// Turing
address public turingHelperAddress;
string public turingUrl;
TuringHelper public turing;
address _turingHelperAddress,
string memory _turingUrl
) {
turingHelperAddress = _turingHelperAddress;
turing = TuringHelper(_turingHelperAddress);
turingUrl = _turingUrl;
function getBobaFaucet(
bytes32 _uuid,
string memory _key
) external {
require(BobaClaimRecords[msg.sender] + waitingPeriod < block.timestamp, 'Invalid request');
// The key is hashed
bytes32 hashedKey = keccak256(abi.encodePacked(_key));
uint256 result = _verifyKey(_uuid, hashedKey);
require(result == 1, 'Captcha wrong');
BobaClaimRecords[msg.sender] = block.timestamp;
IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);
emit GetBobaFaucet(_uuid, hashedKey, msg.sender, BobaFaucetAmount, block.timestamp);
// Call Turing contract to get the result from the outside API endpoint
function _verifyKey(bytes32 _uuid, bytes32 _key) private returns (uint256) {
bytes memory encRequest = abi.encodePacked(_uuid, _key);
bytes memory encResponse = turing.TuringTx(turingUrl, encRequest);
uint256 result = abi.decode(encResponse,(uint256));
emit VerifyKey(turingUrl, _uuid, _key, result);
return result;

Step 3: Funding Turing Helper Contract

We charge 0.01 BOBA for each Turing request and it's based on the Turing Helper Contract.
// Deploy Turing Helper Contract
const Factory__TuringHelperr = new ContractFactory(
(hre as any).deployConfig.deployer_l2
const TuringHelper = await Factory__TuringHelperr.deploy()
await TuringHelper.deployTransaction.wait()
console.log(`TuringHelper was deployed at: ${TuringHelper.address}`)
// Approve Boba Token to add funds to Turing Credit Contract
const BobaTuringCredit = new Contract(
(hre as any).deployConfig.BobaTuringCreditAddress,
(hre as any).deployConfig.deployer_l2
const approveTx = await L2BobaToken.approve(
await approveTx.wait()
const addCreditTx = await BobaTuringCredit.addBalanceTo(
await addCreditTx.wait()
// Add permission for Boba Faucet contract
const addPermissionTx = await TuringHelper.addPermittedCaller(
await addPermissionTx.wait()
Last modified 18d ago