Skip to content
Open
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yieldxyz/shield",
"version": "1.3.0",
"version": "1.4.0",
"description": "Zero-trust transaction validation library for Yield.xyz integrations.",
"packageManager": "pnpm@10.33.1",
"engines": {
Expand Down
68 changes: 68 additions & 0 deletions src/json/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,74 @@ describe('handleJsonRequest', () => {
});
});

describe('schema: args.amount and args.decimals boundaries', () => {
const userAddress = '0x742d35cc6634c0532925a3b844bc9e7595f0beb8';
const referralAddress = '0x371240E80Bf84eC2bA8b55aE2fD0B467b16Db2be';
const validLidoStakeTx = {
to: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84',
from: userAddress,
value: '0xde0b6b3a7640000',
data: '0xa1903eab' + referralAddress.slice(2).padStart(64, '0'),
chainId: 1,
};
// 78 decimal digits — the schema's maxLength for args.amount
const MAX_UINT256_STRING = (2n ** 256n - 1n).toString();
const validRequest = (args: object) => ({
apiVersion: '1.0',
operation: 'validate',
yieldId: 'ethereum-eth-lido-staking',
unsignedTransaction: JSON.stringify(validLidoStakeTx),
userAddress,
args,
});
it('accepts args.decimals as an integer', () => {
const response = call(validRequest({ amount: '1000000', decimals: 6 }));
// Schema accepted (not a SCHEMA_VALIDATION_ERROR) and the request
// proceeded to actual validation.
expect(response.ok).toBe(true);
expect(response.result.isValid).toBe(true);
});
it('accepts args.decimals at the bounds (0 and 255)', () => {
const zero = call(validRequest({ decimals: 0 }));
expect(zero.ok).toBe(true);
const max = call(validRequest({ decimals: 255 }));
expect(max.ok).toBe(true);
});
it('rejects fractional args.decimals', () => {
const response = call(validRequest({ decimals: 6.5 }));
expect(response.ok).toBe(false);
expect(response.error.code).toBe('SCHEMA_VALIDATION_ERROR');
});
it('rejects args.decimals above 255', () => {
const response = call(validRequest({ decimals: 256 }));
expect(response.ok).toBe(false);
expect(response.error.code).toBe('SCHEMA_VALIDATION_ERROR');
});
it('rejects negative args.decimals', () => {
const response = call(validRequest({ decimals: -1 }));
expect(response.ok).toBe(false);
expect(response.error.code).toBe('SCHEMA_VALIDATION_ERROR');
});
it('rejects non-numeric args.decimals', () => {
const response = call(validRequest({ decimals: '6' }));
expect(response.ok).toBe(false);
expect(response.error.code).toBe('SCHEMA_VALIDATION_ERROR');
});
it('accepts a 78-digit args.amount (maxUint256)', () => {
expect(MAX_UINT256_STRING.length).toBe(78); // pin the schema boundary
const response = call(validRequest({ amount: MAX_UINT256_STRING }));
expect(response.ok).toBe(true);
// Lido ignores args.amount today (amount validation is ERC-4626-only in
// Phase 1), so a valid stake tx still validates.
expect(response.result.isValid).toBe(true);
});
it('rejects args.amount longer than 78 characters', () => {
const response = call(validRequest({ amount: '1'.repeat(79) }));
expect(response.ok).toBe(false);
expect(response.error.code).toBe('SCHEMA_VALIDATION_ERROR');
});
});

describe('isSupported operation', () => {
it('should return supported: true for known yield', () => {
const response = call({
Expand Down
1 change: 1 addition & 0 deletions src/json/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const requestSchema = {

// Future use - include for forward compatibility
amount: { type: 'string', maxLength: 78 }, // Max uint256 is 78 digits
decimals: { type: 'integer', minimum: 0, maximum: 255 },
tronResource: { type: 'string', enum: ['BANDWIDTH', 'ENERGY'] },
providerId: { type: 'string', maxLength: 256 },
duration: { type: 'number', minimum: 0 },
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ValidationResult {

export type ActionArguments = {
amount?: string;
decimals?: number; // declared-amount token decimals (intent context)
validatorAddress?: string;
validatorAddresses?: string[];
tronResource?: TronResourceType;
Expand Down
50 changes: 50 additions & 0 deletions src/utils/amount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MAX_UINT256, matchesDeclaredAmount } from './amount';

describe('MAX_UINT256', () => {
it('equals 2^256 - 1', () => {
expect(MAX_UINT256).toBe(2n ** 256n - 1n);
});
});

describe('matchesDeclaredAmount', () => {
const DECLARED = '1000000'; // 1 USDC (6 decimals), wei

it('returns true on exact match', () => {
expect(matchesDeclaredAmount(1000000n, DECLARED)).toBe(true);
});

it('returns false when calldata amount is one above declared', () => {
expect(matchesDeclaredAmount(1000001n, DECLARED)).toBe(false);
});

it('returns false when calldata amount is one below declared', () => {
expect(matchesDeclaredAmount(999999n, DECLARED)).toBe(false);
});

it('returns false on gross inflation', () => {
expect(matchesDeclaredAmount(1000000000000n, DECLARED)).toBe(false);
});

it('returns true when declared is undefined (opt-in skip)', () => {
expect(matchesDeclaredAmount(123456789n, undefined)).toBe(true);
});

it('matches maxUint256 against a declared maxUint256 (sanctioned infinite)', () => {
expect(matchesDeclaredAmount(MAX_UINT256, MAX_UINT256.toString())).toBe(
true,
);
});

it('rejects maxUint256 against a finite declared amount', () => {
expect(matchesDeclaredAmount(MAX_UINT256, DECLARED)).toBe(false);
});

it('matches zero against declared "0"', () => {
expect(matchesDeclaredAmount(0n, '0')).toBe(true);
});

it('throws on a malformed declared string (fail-safe: Shield.validate catches per-type throws)', () => {
expect(() => matchesDeclaredAmount(1000000n, '1.5')).toThrow();
expect(() => matchesDeclaredAmount(1000000n, 'abc')).toThrow();
});
});
10 changes: 10 additions & 0 deletions src/utils/amount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const MAX_UINT256 = (1n << 256n) - 1n;

// Opt-in: undefined declared amount → skip (returns true).
export function matchesDeclaredAmount(
calldataAmount: bigint,
declared?: string,
): boolean {
if (declared === undefined) return true;
return calldataAmount === BigInt(declared);
}
Loading
Loading