Skip to content

omgnetwork/boba-faucet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Boba Faucet

Overview

Boba Faucet is a system for getting Boba Rinkeby ETH and Boba Rinkeby Boba Token. It's implemented using the Turing. Turing is a system for interacting with the outside world from within solidity smart contracts.

Before claiming the token, users have to answer the CAPTCHA. The answer is hashed and compared off-chain via Turing. Once the answer is verified, the smart contract releases the funds.

Directory Structure

  • packages: Contains all the typescript packages and contracts
    • contracts: Solidity smart contracts implementing the Boba Faucet
    • gateway: The Boba Web faucet
    • deployment: Boba faucet contract addresses
    • api: Boba faucet backend API

Specification

This procedure takes place in five steps:

  1. User gets the CAPTCHA image and UUID of the image

    The API GET request is sent to https://api-turing.boba.network/get.catcha on the frontend. The returned payload is

    {
      "UUID": "BYTES32",
      "imageBase64": "CAPTCHA IMAGE"
    }

    The UUID and hashed CAPTCHA answer are stored in the AWS Redis.

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

    const BobaFaucet = new ethers.Contract(
      BOBA_FAUCET_CONTRACE_ADDRESS,
      BobaFaucetJson.abi,
      this.provider.getSigner()
    )
    const tx = await BobaFaucet.getBobaFaucet(uuid, answer)
    await tx.wait()
  3. Geth sends the request to the backend and retrieves the result

    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);
    

    The POST request is sent to https://api-turing.boba.network/verify.captcha . 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)
        else:
            # 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 only need to 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, 'Invalid key and UUID');
    IERC20(BobaAddress).safeTransfer(msg.sender, BobaFaucetAmount);
  5. User gets the funds if the answer is correct or the error message image

Implementation

Step1: Creating API endpoints

Two simple API endpoints are created.

get.captcha

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'),
port=config.get('REDIS_PORT'))

# 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') }

verify.captcha

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'),
                         port=config.get('REDIS_PORT'))

  # 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)
  else:
    return returnPayload(False)

Step2: Creating Boba Faucet Contract

The smart contract imports 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;
    
    constructor(
        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, 'Invalid key and UUID');

        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;
    }
}

Step3: 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(
  TuringHelperJson.abi,
  TuringHelperJson.bytecode,
  (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,
  BobaTuringCreditJson.abi,
  (hre as any).deployConfig.deployer_l2
)
const approveTx = await L2BobaToken.approve(
  BobaTuringCredit.address,
  utils.parseEther('100')
)
await approveTx.wait()

const addCreditTx = await BobaTuringCredit.addBalanceTo(
  utils.parseEther('100'),
  TuringHelper.address
)
await addCreditTx.wait()

// Add permission for Boba Faucet contract
const addPermissionTx = await TuringHelper.addPermittedCaller(
  BobaFaucet.address
)
await addPermissionTx.wait()