Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Oracle account deserialization error handling #115

Merged
merged 4 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions clients/js/src/generated/errors/mplCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,15 +526,15 @@ export class RequiresLifecycleCheckError extends ProgramError {
codeToErrorMap.set(0x24, RequiresLifecycleCheckError);
nameToErrorMap.set('RequiresLifecycleCheck', RequiresLifecycleCheckError);

/** DuplicateLifecycleChecks: Duplicate lifecycle checks were provided for external plugin */
/** DuplicateLifecycleChecks: Duplicate lifecycle checks were provided for external plugin */
export class DuplicateLifecycleChecksError extends ProgramError {
override readonly name: string = 'DuplicateLifecycleChecks';

readonly code: number = 0x25; // 37

constructor(program: Program, cause?: Error) {
super(
'Duplicate lifecycle checks were provided for external plugin ',
'Duplicate lifecycle checks were provided for external plugin',
program,
cause
);
Expand All @@ -543,6 +543,19 @@ export class DuplicateLifecycleChecksError extends ProgramError {
codeToErrorMap.set(0x25, DuplicateLifecycleChecksError);
nameToErrorMap.set('DuplicateLifecycleChecks', DuplicateLifecycleChecksError);

/** InvalidOracleAccountData: Could not read from oracle account */
export class InvalidOracleAccountDataError extends ProgramError {
override readonly name: string = 'InvalidOracleAccountData';

readonly code: number = 0x26; // 38

constructor(program: Program, cause?: Error) {
super('Could not read from oracle account', program, cause);
}
}
codeToErrorMap.set(0x26, InvalidOracleAccountDataError);
nameToErrorMap.set('InvalidOracleAccountData', InvalidOracleAccountDataError);

/**
* Attempts to resolve a custom program error from the provided error code.
* @category Errors
Expand Down
140 changes: 139 additions & 1 deletion clients/js/test/externalPlugins/oracle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
preconfiguredAssetPdaCustomOffsetSet,
close,
} from '@metaplex-foundation/mpl-core-oracle-example';
import { generateSigner } from '@metaplex-foundation/umi';
import { generateSigner, sol } from '@metaplex-foundation/umi';
import { ExternalValidationResult } from '@metaplex-foundation/mpl-core-oracle-example/dist/src/hooked';
import { generateSignerWithSol } from '@metaplex-foundation/umi-bundle-tests';
import {
Expand All @@ -45,6 +45,7 @@ import {
updatePlugin,
fetchAssetV1,
} from '../../src';
import { createAccount } from '@metaplex-foundation/mpl-toolbox';

const createUmi = async () =>
(await baseCreateUmi()).use(mplCoreOracleExample());
Expand Down Expand Up @@ -2388,3 +2389,140 @@ test('it can update oracle to different size external plugin', async (t) => {
],
});
});

test('create fails but does not panic when oracle account does not exist', async (t) => {
const umi = await createUmi();
const oracleSigner = generateSigner(umi);

const asset = generateSigner(umi);
const result = create(umi, {
asset,
name: 'Test name',
uri: 'https://example.com',
plugins: [
{
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
lifecycleChecks: {
create: [CheckResult.CAN_REJECT],
update: [CheckResult.CAN_REJECT],
transfer: [CheckResult.CAN_REJECT],
burn: [CheckResult.CAN_REJECT],
},
baseAddress: oracleSigner.publicKey,
},
],
}).sendAndConfirm(umi);

await t.throwsAsync(result, { name: 'InvalidOracleAccountData' });
});

test('transfer fails but does not panic when oracle account does not exist', async (t) => {
const umi = await createUmi();
const oracleSigner = generateSigner(umi);

const asset = await createAsset(umi, {
plugins: [
{
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
lifecycleChecks: {
transfer: [CheckResult.CAN_REJECT],
},
baseAddress: oracleSigner.publicKey,
},
],
});

await assertAsset(t, umi, {
...DEFAULT_ASSET,
asset: asset.publicKey,
owner: umi.identity.publicKey,
oracles: [
{
type: 'Oracle',
resultsOffset: {
type: 'Anchor',
},
authority: {
type: 'UpdateAuthority',
},
baseAddress: oracleSigner.publicKey,
lifecycleChecks: {
transfer: [CheckResult.CAN_REJECT],
},
pda: undefined,
},
],
});

const newOwner = generateSigner(umi);
const result = transfer(umi, {
asset,
newOwner: newOwner.publicKey,
}).sendAndConfirm(umi);

await t.throwsAsync(result, { name: 'InvalidOracleAccountData' });
});

test('transfer fails but does not panic when oracle account is too small', async (t) => {
const umi = await createUmi();
const newAccount = generateSigner(umi);

// Create an invalid oracle account that is an account with 3 bytes.
await createAccount(umi, {
newAccount,
lamports: sol(0.1),
space: 3,
programId: umi.programs.get('mplCore').publicKey,
}).sendAndConfirm(umi);

const asset = await createAsset(umi, {
plugins: [
{
type: 'Oracle',
resultsOffset: {
type: 'NoOffset',
},
lifecycleChecks: {
transfer: [CheckResult.CAN_REJECT],
},
baseAddress: newAccount.publicKey,
},
],
});

await assertAsset(t, umi, {
...DEFAULT_ASSET,
asset: asset.publicKey,
owner: umi.identity.publicKey,
oracles: [
{
type: 'Oracle',
resultsOffset: {
type: 'NoOffset',
},
authority: {
type: 'UpdateAuthority',
},
baseAddress: newAccount.publicKey,
lifecycleChecks: {
transfer: [CheckResult.CAN_REJECT],
},
pda: undefined,
},
],
});

const newOwner = generateSigner(umi);
const result = transfer(umi, {
asset,
newOwner: newOwner.publicKey,
}).sendAndConfirm(umi);

await t.throwsAsync(result, { name: 'InvalidOracleAccountData' });
});
5 changes: 4 additions & 1 deletion clients/rust/src/generated/errors/mpl_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ pub enum MplCoreError {
#[error("External plugin must have at least one lifecycle check")]
RequiresLifecycleCheck,
/// 37 (0x25) - Duplicate lifecycle checks were provided for external plugin
#[error("Duplicate lifecycle checks were provided for external plugin ")]
#[error("Duplicate lifecycle checks were provided for external plugin")]
DuplicateLifecycleChecks,
/// 38 (0x26) - Could not read from oracle account
#[error("Could not read from oracle account")]
InvalidOracleAccountData,
}

impl solana_program::program_error::PrintProgramError for MplCoreError {
Expand Down
7 changes: 6 additions & 1 deletion idls/mpl_core.json
Original file line number Diff line number Diff line change
Expand Up @@ -4209,7 +4209,12 @@
{
"code": 37,
"name": "DuplicateLifecycleChecks",
"msg": "Duplicate lifecycle checks were provided for external plugin "
"msg": "Duplicate lifecycle checks were provided for external plugin"
},
{
"code": 38,
"name": "InvalidOracleAccountData",
"msg": "Could not read from oracle account"
}
],
"metadata": {
Expand Down
6 changes: 5 additions & 1 deletion programs/mpl-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,12 @@ pub enum MplCoreError {
RequiresLifecycleCheck,

/// 37 - Duplicate lifecycle checks were provided for external plugin
#[error("Duplicate lifecycle checks were provided for external plugin ")]
#[error("Duplicate lifecycle checks were provided for external plugin")]
DuplicateLifecycleChecks,

/// 38 - Could not read from oracle account
#[error("Could not read from oracle account")]
InvalidOracleAccountData,
}

impl PrintProgramError for MplCoreError {
Expand Down
21 changes: 19 additions & 2 deletions programs/mpl-core/src/plugins/oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,18 @@ impl Oracle {
.ok_or(MplCoreError::MissingExternalAccount)?;

let offset = self.results_offset.to_offset_usize();
let validation_result =
OracleValidation::deserialize(&mut &(*oracle_account.data).borrow()[offset..])?;

let oracle_data = (*oracle_account.data).borrow();
let mut oracle_data_slice = oracle_data
.get(offset..)
.ok_or(MplCoreError::InvalidOracleAccountData)?;

if oracle_data_slice.len() < OracleValidation::serialized_size() {
return Err(MplCoreError::InvalidOracleAccountData.into());
}

let validation_result = OracleValidation::deserialize(&mut oracle_data_slice)
.map_err(|_| MplCoreError::InvalidOracleAccountData)?;

match validation_result {
OracleValidation::V1 {
Expand Down Expand Up @@ -187,3 +197,10 @@ pub enum OracleValidation {
update: ExternalValidationResult,
},
}

impl OracleValidation {
/// Borsh- and Anchor-serialized size of the `OracleValidation` struct.
pub fn serialized_size() -> usize {
5
}
}