Skip to content
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
45 changes: 0 additions & 45 deletions .github/workflows/contracts.yml

This file was deleted.

49 changes: 16 additions & 33 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, String, Symbol, Vec,
};

// Configuration constants for escrow contract
const SECONDS_PER_DAY: u64 = 86_400;
// Maximum allowed commitment amount (example limit)
const MAX_AMOUNT: i128 = 1_000_000_000_000;
// Maximum allowed duration in days
const MAX_DURATION_DAYS: u32 = 365;
// Maximum penalty basis points (100% = 10_000 bps)
const MAX_PENALTY_BPS: u32 = 10_000;

/// Storage keys for persistent contract state.
#[contracttype]
#[derive(Clone)]
Expand Down Expand Up @@ -180,22 +189,7 @@ pub struct EarlyExitResult {
pub finalStatus: EscrowStatus,
}

const MAX_PENALTY_BPS: u32 = 10_000;
const SECONDS_PER_DAY: u64 = 86_400;
const YIELD_BPS_DENOMINATOR: i128 = 3_650_000; // 365 days * 10_000 bps

fn yield_rate_bps(risk: RiskProfile) -> u32 {
match risk {
RiskProfile::Safe => 500,
RiskProfile::Balanced => 700,
RiskProfile::Aggressive => 1_000,
}
}

fn calculate_accrued_yield(amount: i128, duration_days: u32, risk: RiskProfile) -> i128 {
let rate_bps = yield_rate_bps(risk) as i128;
(amount * rate_bps * duration_days as i128) / YIELD_BPS_DENOMINATOR
}

#[contract]
pub struct EscrowContract;
Expand Down Expand Up @@ -320,33 +314,22 @@ impl EscrowContract {
if amount <= 0 {
return Err(Error::InvalidAmount);
}
if amount > MAX_AMOUNT {
return Err(Error::InvalidAmount);
}
if duration_days == 0 {
return Err(Error::InvalidDuration);
}
if duration_days > MAX_DURATION_DAYS {
return Err(Error::InvalidDuration);
}
if penalty_bps > MAX_PENALTY_BPS {
return Err(Error::PenaltyTooHigh);
}

let id = Self::next_id(&env);
let now = env.ledger().timestamp();

// Guard against overflow when converting duration_days into an absolute
// maturity timestamp. Overflow must never wrap, otherwise commitments
// could be released/refunded at incorrect times.
//
// NOTE: Soroban client bindings may pass arguments in ways that can hide
// the expected arithmetic overflow during tests. Explicitly reject
// impossible duration ranges before doing any conversions.
let max_duration_days = (u64::MAX / SECONDS_PER_DAY) as u32;
if duration_days > max_duration_days {
return Err(Error::InvalidDuration);
}

let duration_seconds = (duration_days as u64) * SECONDS_PER_DAY;
let maturity = now
.checked_add(duration_seconds)
.ok_or(Error::InvalidDuration)?;

let maturity = now.checked_add((duration_days as u64).checked_mul(SECONDS_PER_DAY).ok_or(Error::InvalidDuration)?).ok_or(Error::InvalidDuration)?;

let commitment = Commitment {
id,
Expand Down
30 changes: 30 additions & 0 deletions contracts/escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,36 @@ fn owner_index_tracks_commitments() {
assert_eq!(ids.len(), 2);
assert_eq!(ids.get(0).unwrap(), a);
assert_eq!(ids.get(1).unwrap(), b);

#[test]
fn create_rejects_excessive_amount() {
let f = setup();
let owner = Address::generate(&f.env);
let res = f.client.try_create_commitment(
&owner,
&f.asset,
&(MAX_AMOUNT + 1),
&RiskProfile::Safe,
&(MAX_DURATION_DAYS + 1),
&2000,
);
assert_eq!(res, Err(Ok(Error::InvalidAmount)));
}

#[test]
fn create_rejects_excessive_duration() {
let f = setup();
let owner = Address::generate(&f.env);
let res = f.client.try_create_commitment(
&owner,
&f.asset,
&1_000,
&RiskProfile::Safe,
&(MAX_DURATION_DAYS + 1),
&2000,
);
assert_eq!(res, Err(Ok(Error::InvalidDuration)));
}
}

fn assert_refund_invariants(amount: i128, penalty_bps: u32) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/attestations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const POST = withApiHandler(async (req: NextRequest, _context, correlatio
}

try {
await getCommitmentFromChain(body.commitmentId);
await getCommitmentFromChain(body.commitmentId, { requestId: correlationId });
} catch (err) {
const normalized = normalizeBackendError(err, {
code: 'BLOCKCHAIN_CALL_FAILED',
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/commitments/[id]/history/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const GET = withApiHandler(async (
// Resolve commitment — throws NotFoundError (→ 404) if absent
let commitment;
try {
commitment = await getCommitmentFromChain(commitmentId);
commitment = await getCommitmentFromChain(commitmentId, { requestId: correlationId });
} catch {
throw new NotFoundError('Commitment', { commitmentId });
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/commitments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const GET = withApiHandler(async (_req: NextRequest, context, correlation

let commitment: any;
try {
commitment = await getCommitmentFromChain(commitmentId);
commitment = await getCommitmentFromChain(commitmentId, { requestId: correlationId });
} catch (err) {
if (err instanceof BackendError && err.code === 'NOT_FOUND') {
throw new NotFoundError('Commitment', { commitmentId });
Expand Down
30 changes: 11 additions & 19 deletions src/app/api/commitments/[id]/settle/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export const POST = withApiHandler(async (req: NextRequest, { params }, correlat
throw new ValidationError('Invalid request data', validation.error.issues);
}

const callerAddress = validation.data.callerAddress;
const commitment: any = await getCommitmentFromChain(id);
const callerAddress = validation.data.callerAddress;
const commitment: any = await getCommitmentFromChain(id, { requestId: correlationId });

if (!commitment) {
throw new NotFoundError('Commitment', { commitmentId: id });
Expand All @@ -80,10 +80,10 @@ export const POST = withApiHandler(async (req: NextRequest, { params }, correlat
throw new ConflictError('Commitment has already been exited early');
}

const settlementResult = await settleCommitmentOnChain({
commitmentId: id,
callerAddress,
});
const settlementResult = await settleCommitmentOnChain({
commitmentId: id,
callerAddress,
}, { requestId: correlationId });

logCommitmentSettled({
ip,
Expand All @@ -101,19 +101,11 @@ export const POST = withApiHandler(async (req: NextRequest, { params }, correlat
txHash: settlementResult.txHash,
reference: settlementResult.reference,
settledAt: new Date().toISOString(),
};

if (idempotencyKey) {
await idempotencyService.complete(idempotencyKey, responseData, 200);
}

return ok(responseData, undefined, 200, correlationId);
} catch (error) {
if (idempotencyKey) {
await idempotencyService.fail(idempotencyKey);
}
throw error;
}
}, { requestId: correlationId },
undefined,
200,
correlationId,
);
}, { cors: COMMITMENT_SETTLE_CORS_POLICY });

const _405 = methodNotAllowed(['POST']);
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/commitments/[id]/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const GET = withApiHandler(async (

let commitment;
try {
commitment = await getCommitmentFromChain(commitmentId);
commitment = await getCommitmentFromChain(commitmentId, { requestId: correlationId });
} catch {
throw new NotFoundError('Commitment', { commitmentId });
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/commitments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const GET = withApiHandler(async (req: NextRequest, _context, correlation
);
}

const commitments = await getUserCommitmentsFromChain(ownerAddress);
const commitments = await getUserCommitmentsFromChain(ownerAddress, { requestId: correlationId });
let mapped = commitments.map((c: any) => ({
commitmentId: String(c.id ?? c.commitmentId),
ownerAddress: c.ownerAddress,
Expand Down Expand Up @@ -131,7 +131,7 @@ export const POST = withApiHandler(async (req: NextRequest, _context, correlatio
durationDays,
maxLossBps,
metadata,
});
}, { requestId: correlationId });

return ok(result, undefined, 201, correlationId);
}, { cors: COMMITMENTS_CORS_POLICY });
Expand Down
Loading
Loading