-
Notifications
You must be signed in to change notification settings - Fork 300
Wp 5782/fix near token enablement validation #6958
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,13 +36,15 @@ import { | |
SignedTransaction, | ||
SignTransactionOptions as BaseSignTransactionOptions, | ||
TokenEnablementConfig, | ||
TransactionExplanation, | ||
TransactionParams, | ||
TransactionType, | ||
VerifyAddressOptions, | ||
VerifyTransactionOptions, | ||
} from '@bitgo/sdk-core'; | ||
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins, Nep141Token, Networks } from '@bitgo/statics'; | ||
|
||
import { KeyPair as NearKeyPair, Transaction, TransactionBuilder, TransactionBuilderFactory } from './lib'; | ||
import { TransactionExplanation, TxData } from './lib/iface'; | ||
import nearUtils from './lib/utils'; | ||
import { MAX_GAS_LIMIT_FOR_FT_TRANSFER } from './lib/constants'; | ||
|
||
|
@@ -1000,6 +1002,10 @@ export class Near extends BaseCoin { | |
const explainedTx = transaction.explainTransaction(); | ||
|
||
// users do not input recipients for consolidation requests as they are generated by the server | ||
if (txParams.type === 'enabletoken' && params.verification?.verifyTokenEnablement) { | ||
this.validateTokenEnablementTransaction(transaction, explainedTx, txParams); | ||
} | ||
|
||
if (txParams.recipients !== undefined) { | ||
if (txParams.type === 'enabletoken') { | ||
const tokenName = explainedTx.outputs[0].tokenName; | ||
|
@@ -1031,6 +1037,18 @@ export class Near extends BaseCoin { | |
}); | ||
|
||
if (!_.isEqual(filteredOutputs, filteredRecipients)) { | ||
// For enabletoken, provide more specific error messages for address mismatches | ||
if (txParams.type === 'enabletoken' && params.verification?.verifyTokenEnablement) { | ||
const mismatchedAddresses = txParams.recipients | ||
?.filter( | ||
(recipient, index) => !filteredOutputs[index] || recipient.address !== filteredOutputs[index].address | ||
) | ||
.map((recipient) => recipient.address); | ||
|
||
if (mismatchedAddresses && mismatchedAddresses.length > 0) { | ||
throw new Error(`Address mismatch: ${mismatchedAddresses.join(', ')}`); | ||
} | ||
} | ||
throw new Error('Tx outputs does not match with expected txParams recipients'); | ||
} | ||
for (const recipients of txParams.recipients) { | ||
|
@@ -1055,4 +1073,212 @@ export class Near extends BaseCoin { | |
} | ||
auditEddsaPrivateKey(prv, publicKey ?? ''); | ||
} | ||
|
||
private validateTokenEnablementTransaction( | ||
transaction: Transaction, | ||
explainedTx: TransactionExplanation, | ||
txParams: TransactionParams | ||
): void { | ||
const transactionData = transaction.toJson(); | ||
this.validateTxType(txParams, explainedTx); | ||
this.validateSigner(transactionData); | ||
this.validateRawReceiver(transactionData, txParams); | ||
this.validatePublicKey(transactionData); | ||
this.validateRawActions(transactionData, txParams); | ||
this.validateBeneficiary(explainedTx, txParams); | ||
this.validateTokenOutput(explainedTx, txParams); | ||
} | ||
|
||
// Validates that the signer ID exists in the transaction | ||
private validateSigner(transactionData: TxData): void { | ||
if (!transactionData.signerId) { | ||
throw new Error('Error on token enablements: missing signer ID in transaction'); | ||
} | ||
} | ||
|
||
private validateBeneficiary(explainedTx: TransactionExplanation, txParams: TransactionParams): void { | ||
if (!explainedTx.outputs || explainedTx.outputs.length === 0) { | ||
throw new Error('Error on token enablements: transaction has no outputs to validate beneficiary'); | ||
} | ||
|
||
// NEAR token enablements only support a single recipient | ||
if (!txParams.recipients || txParams.recipients.length === 0) { | ||
throw new Error('Error on token enablements: missing recipients in transaction parameters'); | ||
} | ||
|
||
if (txParams.recipients.length !== 1) { | ||
throw new Error('Error on token enablements: token enablement only supports a single recipient'); | ||
} | ||
|
||
if (explainedTx.outputs.length !== 1) { | ||
throw new Error('Error on token enablements: transaction must have exactly 1 output'); | ||
} | ||
|
||
const output = explainedTx.outputs[0]; | ||
const recipient = txParams.recipients[0]; | ||
|
||
if (!recipient?.address) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you confirm if near token enablements supports a single recipient? because if that's the case then you may need to check for outputs.length === 1 and if it isn't then you may need to iterate over the outputs or recipients and match one with the other (also you can preliminary compare outputs.length with recipients.length and they should match). |
||
throw new Error('Error on token enablements: missing beneficiary address in transaction parameters'); | ||
} | ||
|
||
if (output.address !== recipient.address) { | ||
throw new Error('Error on token enablements: transaction beneficiary mismatch with user expectation'); | ||
} | ||
} | ||
|
||
// Validates that the raw transaction receiverId matches the expected token contract | ||
private validateRawReceiver(transactionData: TxData, txParams: TransactionParams): void { | ||
if (!transactionData.receiverId) { | ||
throw new Error('Error on token enablements: missing receiver ID in transaction'); | ||
} | ||
|
||
const recipient = txParams.recipients?.[0]; | ||
if (!recipient?.tokenName) { | ||
throw new Error('Error on token enablements: missing token name in transaction parameters'); | ||
} | ||
|
||
const tokenInstance = nearUtils.getTokenInstanceFromTokenName(recipient.tokenName); | ||
if (!tokenInstance) { | ||
throw new Error(`Error on token enablements: unknown token '${recipient.tokenName}'`); | ||
} | ||
|
||
if (transactionData.receiverId !== tokenInstance.contractAddress) { | ||
throw new Error( | ||
`Error on token enablements: receiver contract mismatch - expected '${tokenInstance.contractAddress}', got '${transactionData.receiverId}'` | ||
); | ||
} | ||
} | ||
|
||
// Validates token output information from explained transaction | ||
private validateTokenOutput(explainedTx: TransactionExplanation, txParams: TransactionParams): void { | ||
if (!explainedTx.outputs || explainedTx.outputs.length !== 1) { | ||
throw new Error('Error on token enablements: transaction must have exactly 1 output'); | ||
} | ||
|
||
const output = explainedTx.outputs[0]; | ||
const recipient = txParams.recipients?.[0]; | ||
|
||
if (!output.tokenName) { | ||
throw new Error('Error on token enablements: missing token name in transaction output'); | ||
} | ||
|
||
const tokenInstance = nearUtils.getTokenInstanceFromTokenName(output.tokenName); | ||
if (!tokenInstance) { | ||
throw new Error(`Error on token enablements: unknown token '${output.tokenName}'`); | ||
} | ||
|
||
if (recipient?.tokenName && recipient.tokenName !== output.tokenName) { | ||
throw new Error( | ||
`Error on token enablements: token mismatch - user expects '${recipient.tokenName}', transaction has '${output.tokenName}'` | ||
); | ||
} | ||
} | ||
|
||
private validatePublicKey(transactionData: TxData): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this class has |
||
if (!transactionData.publicKey) { | ||
throw new Error('Error on token enablements: missing public key in transaction'); | ||
} | ||
|
||
// Validate ed25519 format: "ed25519:base58_encoded_key" | ||
if (!transactionData.publicKey.startsWith('ed25519:')) { | ||
throw new Error('Error on token enablements: unsupported key type, expected ed25519'); | ||
} | ||
|
||
// Validate base58 part after "ed25519:" | ||
const base58Part = transactionData.publicKey.substring(8); | ||
if (!base58Part || base58Part.length !== 44) { | ||
// ed25519 keys are 32 bytes = 44 base58 chars | ||
throw new Error('Error on token enablements: invalid ed25519 public key format'); | ||
} | ||
|
||
// Validate it's actually valid base58 | ||
let decoded; | ||
try { | ||
decoded = nearAPI.utils.serialize.base_decode(base58Part); | ||
} catch { | ||
throw new Error('Error on token enablements: invalid base58 encoding in public key'); | ||
} | ||
|
||
if (!decoded || decoded.length !== 32) { | ||
throw new Error('Error on token enablements: invalid ed25519 public key length'); | ||
} | ||
} | ||
|
||
// Validates the raw transaction actions according to NEAR protocol spec | ||
private validateRawActions(transactionData: TxData, txParams: TransactionParams): void { | ||
// Must have exactly 1 action (NEAR spec requirement) | ||
if (!transactionData.actions || transactionData.actions.length !== 1) { | ||
throw new Error('Error on token enablements: must have exactly 1 action'); | ||
} | ||
|
||
const action = transactionData.actions[0]; | ||
|
||
// Must be a functionCall action (not transfer) | ||
if (!action.functionCall) { | ||
throw new Error('Error on token enablements: action must be a function call'); | ||
} | ||
|
||
// Must be storage_deposit method (NEAR spec requirement) | ||
if (action.functionCall.methodName !== 'storage_deposit') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use the |
||
throw new Error( | ||
`Error on token enablements: invalid method '${action.functionCall.methodName}', expected 'storage_deposit'` | ||
); | ||
} | ||
|
||
// Validate args structure (should be JSON object) | ||
if (!action.functionCall.args || typeof action.functionCall.args !== 'object') { | ||
throw new Error('Error on token enablements: invalid or missing function call arguments'); | ||
} | ||
|
||
// Validate deposit exists and is valid | ||
if (!action.functionCall.deposit) { | ||
throw new Error('Error on token enablements: missing deposit in function call'); | ||
} | ||
|
||
const depositAmount = new BigNumber(action.functionCall.deposit); | ||
if (depositAmount.isNaN() || depositAmount.isLessThan(0)) { | ||
throw new Error('Error on token enablements: invalid deposit amount in function call'); | ||
} | ||
|
||
// Validate gas exists and is valid | ||
if (!action.functionCall.gas) { | ||
throw new Error('Error on token enablements: missing gas in function call'); | ||
} | ||
|
||
const gasAmount = new BigNumber(action.functionCall.gas); | ||
if (gasAmount.isNaN() || gasAmount.isLessThan(0)) { | ||
throw new Error('Error on token enablements: invalid gas amount in function call'); | ||
} | ||
|
||
// Validate deposit amount against expected storage deposit (merged from validateActions) | ||
const recipient = txParams.recipients?.[0]; | ||
if (recipient?.tokenName) { | ||
const tokenInstance = nearUtils.getTokenInstanceFromTokenName(recipient.tokenName); | ||
if (tokenInstance?.storageDepositAmount && action.functionCall.deposit !== tokenInstance.storageDepositAmount) { | ||
throw new Error( | ||
`Error on token enablements: deposit amount ${action.functionCall.deposit} does not match expected storage deposit ${tokenInstance.storageDepositAmount}` | ||
); | ||
} | ||
} | ||
|
||
// Validate user-specified amount matches deposit (merged from validateActions) | ||
if ( | ||
recipient?.amount !== undefined && | ||
recipient.amount !== '0' && | ||
recipient.amount !== action.functionCall.deposit | ||
) { | ||
throw new Error( | ||
`Error on token enablements: user specified amount '${recipient.amount}' does not match storage deposit '${action.functionCall.deposit}'` | ||
); | ||
} | ||
} | ||
|
||
private validateTxType(txParams: TransactionParams, explainedTx: TransactionExplanation): void { | ||
const expectedType = TransactionType.StorageDeposit; | ||
const actualType = explainedTx.type; | ||
|
||
if (actualType !== expectedType) { | ||
throw new Error(`Invalid transaction type on token enablement: expected "${expectedType}", got "${actualType}".`); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JSDoc