Encifher

Onchain Decryption (Token Withdrawal)

Flow for decryption of value for token withdrawal

Overview

In the current setup the only usecase of enabling onchain decryption value is to enable users to withdraw their token basically converting encrypted tokens back to regular SPL token and crediting those tokens to receiver address.

Decryption Flow

There are laregely three steps by which the onchain decryption flow works:

  • Withdraw call: Users call for unwrapping/withdrawal of funds by calling process_withdraw() function of wrapper program.
  • Offchain Decryption: Offchain decryption relayer picks-up the events emitted by the process_withdraw() function which includes token pointers to decrypt, destination address, token mint add etc.
  • Onchain Withdraw Callback call: Offchain decryption relayer submits a signature on a something called request_id which is derived from token pointer (pointer to decrypt), destination address, token mint address and few other params. This signature gets verified onchain and funds are credited to the destination address.

Withdraw call

Use calls the process_withdraw function and passes certain parameters which are to (pubkey where the funds needs to be sent), amount (funds to withdraw) and pt_amount (plaintext amount of the funds to withdraw this plaintext value acts are an agreement between user withdrawal request and plaintext amount suggested by the relayer);

pub fn process_withdraw(ctx: Context<BurnAndWithdraw>, to: Pubkey, amount: Einput, pt_amount: u64) -> Result<()> {
    let burn_accounts = MintTo {
        emint: ctx.accounts.etoken_mint.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        token_account: ctx.accounts.from_etoken_account.to_account_info(),
        authority: ctx.accounts.from_authority.to_account_info(),
        pet_executor_program: ctx.accounts.pet_executor_program.to_account_info(),
        executor: ctx.accounts.executor.to_account_info(),
        core_manager: ctx.accounts.core_manager.to_account_info(),
    };
    let burn_program = ctx.accounts.etoken_program.to_account_info();
    let burn_ctx = CpiContext::new(burn_program, burn_accounts);
    let burn_result = etoken::cpi::burn(burn_ctx, ctx.accounts.from_authority.key(), amount.clone())?;
    let burn_amount: pet_executor::types::Euint64 = burn_result.get();
    let passed_amount = pet_executor::types::Euint64::new(amount.handle, amount.proof, Vec::new());

    let decryption_request = &mut ctx.accounts.decryption_request;
    decryption_request.token_mint = ctx.accounts.mint.key();
    decryption_request.to_token_account = to;
    decryption_request.pt_amount = pt_amount;
    decryption_request.is_processed = false;

    let decrypt_accounts = Execute {
        executor: ctx.accounts.executor.to_account_info(),
        signer: ctx.accounts.from_authority.to_account_info(),
    };
    let decrypt_program = ctx.accounts.pet_executor_program.to_account_info();
    let decrypt_ctx = CpiContext::new(decrypt_program, decrypt_accounts);
    pet_executor::cpi::decrypt(
        decrypt_ctx,
        ctx.accounts.mint.key(),
        to,
        pt_amount,
        passed_amount,
        burn_amount,
    )?;

    Ok(())
}

Within process_withdraw function it derives a unique PDA address from the function parameters (amount, pt_amount, token mint, to) as referenced below

    #[account(
        init,
        payer = payer,
        space = DecryptionRequest::LEN,
        seeds = [
            b"decryption_request", 
            amount.handle.to_le_bytes().as_ref(), 
            pt_amount.to_le_bytes().as_ref(), 
            to.as_ref(), 
            mint.key().as_ref()
        ],
        bump,
    )]
    pub decryption_request: Account<'info, DecryptionRequest>,

And at that PDA address it saves the necessary information regarding the decryption/withdrawal request which needs to be processed. Post creation of PDA the same function calls emit the decryption event which will be listened by Offchain decryption relayer.

Offchain Decryption

Offchain decryption relayer listenes to the onchain decryption event and extract out the necessary parameters required for withdrawal. It decrypts the amount pointer corresponding to which withdrawal needs to happen, Along with it signs the request_id the signature here works as a attestation that the plaintext value relayed onchain by the offchain decryption relayer was correct and the user also has the access to get the value decrypted (checked via ACL).

Onchain Withdraw Callback call

Post decryption of the pointer (user encrypted token) the relayer makes an onchain call with various parameters which includes amount pointer, plaintext amount, signature, recovery id etc. In the callback call we again calculate the same PDA address to check weather the decryption request hasn't been processed multiple time. The PDA address per request id works as a nullifier to keep check on unique withdrawals. During withdrawal callback it checks for the request_id integrity and uniqueness post that it verifies the signature over the request id making sure that decryption relayer posted a trusted plaintext value onchain. Once the signature verification is completed the function does a couple of checks and post that credits funds corresponding to the pointer to the receiver address.

pub fn process_withdraw_callback(
    ctx: Context<WithdrawCallback>,
    amount: Einput,
    burn_amount: Einput, 
    pt_amount: u64,
    signature: [u8; 64],
    recovery_id: u8,
) -> Result<()> {
    let decryption_request = &mut ctx.accounts.decryption_request;

    let (expected_pda, bump) = Pubkey::find_program_address(
        &[
            b"decryption_request", 
            amount.handle.to_le_bytes().as_ref(), 
            pt_amount.to_le_bytes().as_ref(), 
            ctx.accounts.user_token_account.owner.as_ref(), 
            ctx.accounts.mint.key().as_ref(),
        ],
        ctx.program_id,
    );
    msg!("program id: {:?}", ctx.program_id);
    msg!("Expected PDA: {:?}", expected_pda);
    msg!("Expected bump: {:?}", bump);
    msg!("Provided PDA: {:?}", decryption_request.key());
    msg!("amount handle: {:?}", amount.handle.to_le_bytes().as_ref());
    msg!("pt_amount handle: {:?}", pt_amount.to_le_bytes().as_ref());
    msg!("TA owner: {:?}", ctx.accounts.user_token_account.owner.as_ref());
    msg!("Mint: {:?}", ctx.accounts.mint.key().as_ref());

    require!(expected_pda == decryption_request.key(), WithdrawError::DecryptionRequestPDAMismatch);
    // Verify the request is not already processed
    require!(!decryption_request.is_processed, WithdrawError::RequestProcessed);

    let request_id = get_request_id(
        burn_amount.handle, 
        pt_amount, 
        ctx.accounts.user_token_account.owner, 
        ctx.accounts.mint.key(),
    );

    // hiding signature check for now
    let coprocessor_key = ctx.accounts.core_manager.registered_coprocessors[
        ctx.accounts.core_manager.active_coprocessor_index as usize
        ].coprocessor_derived_public_key;
    msg!("Verifying signature... {} pubkey: {:?}", recovery_id, coprocessor_key);
    let is_valid = verify_signature(request_id, signature, recovery_id, coprocessor_key)?;
    require!(is_valid, WithdrawError::InvalidSignature);
    
    let cpi_accounts = TransferChecked {
        from: ctx.accounts.program_token_account.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        to: ctx.accounts.user_token_account.to_account_info(),
        authority: ctx.accounts.program_token_account.to_account_info(),
    };
    let cpi_program = ctx.accounts.token_program.to_account_info();
    let mint_key = ctx.accounts.mint.key();
    let signer_seeds: &[&[&[u8]]] = &[
        &[
            b"ewrapper_account",
            mint_key.as_ref(),
            &[ctx.bumps.program_token_account],
        ],
    ];
    let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts).with_signer(signer_seeds);
    transfer_checked(cpi_ctx, pt_amount, ctx.accounts.mint.decimals)?;
    
    // Mark the request as processed
    decryption_request.is_processed = true;
    
    Ok(())
}