Skip to content

Commit bbca051

Browse files
author
Iztok
committed
veriify agent bot redemption state before state changes
1 parent 114283e commit bbca051

File tree

3 files changed

+49
-24
lines changed

3 files changed

+49
-24
lines changed

packages/fasset-bots-core/src/actors/AgentBotRedemption.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class AgentBotRedemption {
8484

8585
async redemptionDefault(rootEm: EM, args: EventArgs<RedemptionDefault>) {
8686
await this.bot.runInTransaction(rootEm, async (em) => {
87-
const redemption = await this.findRedemption(em, { requestId: toBN(args.requestId) });
87+
const redemption = await this.findRedemption(em, { requestId: toBN(args.requestId) }, LockMode.PESSIMISTIC_WRITE);
8888
redemption.defaulted = true;
8989
if (redemption.state === AgentRedemptionState.UNPAID || redemption.state === AgentRedemptionState.REJECTED) {
9090
redemption.finalState = this.getFinalState(redemption);
@@ -101,7 +101,7 @@ export class AgentBotRedemption {
101101
* @param agentVault agent's vault address
102102
*/
103103
private async finishRedemption(rootEm: EM, rd: { requestId: BNish }, finalState: AgentRedemptionFinalState) {
104-
await this.updateRedemption(rootEm, { requestId: toBN(rd.requestId) }, {
104+
await this.updateRedemption(rootEm, { requestId: toBN(rd.requestId) }, null, {
105105
state: AgentRedemptionState.DONE,
106106
finalState: finalState,
107107
});
@@ -125,7 +125,7 @@ export class AgentBotRedemption {
125125
/* istanbul ignore next */
126126
if (this.bot.stopRequested()) return;
127127
try {
128-
await this.handleOpenRedemption(rootEm, state, redemption);
128+
await this.handleActiveRedemptionInState(rootEm, state, redemption);
129129
} catch (error) {
130130
logger.error(`Error handling redemption ${redemption.requestId} in state ${state}`, error);
131131
}
@@ -181,7 +181,7 @@ export class AgentBotRedemption {
181181
.getResultList();
182182
}
183183

184-
async handleOpenRedemption(rootEm: EM, state: AgentRedemptionState, redemption: Readonly<AgentRedemption>) {
184+
async handleActiveRedemptionInState(rootEm: EM, state: AgentRedemptionState, redemption: Readonly<AgentRedemption>) {
185185
switch (state) {
186186
case AgentRedemptionState.STARTED:
187187
await this.checkBeforeRedemptionPayment(rootEm, redemption);
@@ -228,7 +228,7 @@ export class AgentBotRedemption {
228228
throw error;
229229
}
230230
}
231-
redemption = await this.updateRedemption(rootEm, redemption, {
231+
redemption = await this.updateRedemption(rootEm, redemption, null, {
232232
state: AgentRedemptionState.DONE,
233233
finalState: finalState ?? this.getFinalState(redemption),
234234
});
@@ -272,7 +272,7 @@ export class AgentBotRedemption {
272272
Time expired on underlying chain. Last block for payment was ${redemption.lastUnderlyingBlock}
273273
with timestamp ${redemption.lastUnderlyingTimestamp}. Current block is ${lastFinalizedBlock.number}
274274
with timestamp ${lastFinalizedBlock.timestamp}.`);
275-
redemption = await this.updateRedemption(rootEm, redemption, {
275+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.STARTED, {
276276
state: AgentRedemptionState.UNPAID,
277277
});
278278
}
@@ -288,11 +288,11 @@ export class AgentBotRedemption {
288288
const poolFeeUBA = redemptionFee.mul(redemptionPoolFeeShareBIPS).divn(MAX_BIPS);
289289
let maxRedemptionFee = redemptionFee.sub(poolFeeUBA);
290290

291-
// special redemption ticket - `transferToCoreVault`
292-
if (maxRedemptionFee.eq(BN_ZERO)) {
293-
const coreVaultSourceAddress = await requireNotNull(this.context.coreVaultManager).coreVaultAddress();
294-
if (redemption.paymentAddress === coreVaultSourceAddress) { // additional check
295-
maxRedemptionFee = await this.bot.getTransferToCoreVaultMaxFee(redemption.paymentAddress, redemption.valueUBA)
291+
// for transfers to core vault, check the free underlying can cover transaction fee, since there is no redemption fee
292+
if (maxRedemptionFee.eq(BN_ZERO) && this.context.coreVaultManager != null) {
293+
const coreVaultSourceAddress = await this.context.coreVaultManager.coreVaultAddress();
294+
if (redemption.paymentAddress === coreVaultSourceAddress) { // is it transfer to core vault?
295+
maxRedemptionFee = await this.bot.getTransferToCoreVaultMaxFee(redemption.paymentAddress, redemption.valueUBA);
296296
if (maxRedemptionFee.eq(BN_ZERO)) {
297297
logger.error(`Cannot pay for redemption ${redemption.requestId}, current fee is greater than agent can spend.`);
298298
await this.notifier.sendTransferToCVRedemptionNoFreeUnderlying(redemption.requestId);
@@ -304,7 +304,7 @@ export class AgentBotRedemption {
304304
}
305305
}
306306

307-
redemption = await this.updateRedemption(rootEm, redemption, {
307+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.STARTED, {
308308
state: AgentRedemptionState.PAYING,
309309
});
310310
try {
@@ -319,7 +319,7 @@ export class AgentBotRedemption {
319319
feeSourceAddress: feeSourceAddress
320320
});
321321
});
322-
redemption = await this.updateRedemption(rootEm, redemption, {
322+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.PAYING, {
323323
txDbId: txDbId,
324324
state: AgentRedemptionState.PAID,
325325
});
@@ -345,7 +345,7 @@ export class AgentBotRedemption {
345345
const request = await this.bot.locks.nativeChainLock(this.bot.requestSubmitterAddress()).lockAndRun(async () => {
346346
return await this.context.attestationProvider.requestAddressValidityProof(redemption.paymentAddress);
347347
});
348-
redemption = await this.updateRedemption(rootEm, redemption, {
348+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.STARTED, {
349349
state: AgentRedemptionState.REQUESTED_REJECTION_PROOF,
350350
proofRequestRound: request.round,
351351
proofRequestData: request.data,
@@ -382,7 +382,7 @@ export class AgentBotRedemption {
382382
await this.bot.locks.nativeChainLock(this.bot.owner.workAddress).lockAndRun(async () => {
383383
await this.context.assetManager.rejectInvalidRedemption(web3DeepNormalize(proof), redemption.requestId, { from: this.agent.owner.workAddress });
384384
});
385-
redemption = await this.updateRedemption(rootEm, redemption, {
385+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.REQUESTED_REJECTION_PROOF, {
386386
state: AgentRedemptionState.DONE,
387387
finalState: AgentRedemptionFinalState.REJECTED,
388388
});
@@ -403,7 +403,7 @@ export class AgentBotRedemption {
403403
await this.notifier.sendRedemptionAddressValidationNoProof(redemption.requestId,
404404
redemption.proofRequestRound, redemption.proofRequestData, redemption.paymentAddress);
405405
logger.info(`Agent ${this.agent.vaultAddress} will retry obtaining address validation proof for redemption ${redemption.requestId}.`);
406-
redemption = await this.updateRedemption(rootEm, redemption, {
406+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.REQUESTED_REJECTION_PROOF, {
407407
state: AgentRedemptionState.STARTED,
408408
proofRequestRound: undefined,
409409
proofRequestData: undefined,
@@ -436,7 +436,7 @@ export class AgentBotRedemption {
436436
const info = await this.context.wallet.checkTransactionStatus(redemption.txDbId);
437437
if (info.status == TransactionStatus.TX_SUCCESS || info.status == TransactionStatus.TX_FAILED) {
438438
if (info.transactionHash) {
439-
redemption = await this.updateRedemption(rootEm, redemption, {
439+
redemption = await this.updateRedemption(rootEm, redemption, null, {
440440
txHash: info.transactionHash
441441
});
442442
assertNotNull(redemption.txHash);
@@ -448,7 +448,7 @@ export class AgentBotRedemption {
448448
info.replacedByStatus == TransactionStatus.TX_SUCCESS || info.replacedByStatus == TransactionStatus.TX_FAILED
449449
)) {
450450
if (info.replacedByHash) {
451-
redemption = await this.updateRedemption(rootEm, redemption, {
451+
redemption = await this.updateRedemption(rootEm, redemption, null, {
452452
txHash: info.replacedByHash
453453
});
454454
assertNotNull(redemption.txHash);
@@ -471,7 +471,7 @@ export class AgentBotRedemption {
471471
const request = await this.bot.locks.nativeChainLock(this.bot.requestSubmitterAddress()).lockAndRun(async () => {
472472
return await this.context.attestationProvider.requestPaymentProof(txHash, this.agent.underlyingAddress, redemption.paymentAddress);
473473
});
474-
redemption = await this.updateRedemption(rootEm, redemption, {
474+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.PAID, {
475475
state: AgentRedemptionState.REQUESTED_PROOF,
476476
proofRequestRound: request.round,
477477
proofRequestData: request.data,
@@ -518,7 +518,7 @@ export class AgentBotRedemption {
518518
throw error;
519519
}
520520
}
521-
redemption = await this.updateRedemption(rootEm, redemption, {
521+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.REQUESTED_PROOF, {
522522
state: AgentRedemptionState.DONE,
523523
});
524524
logger.info(`Agent ${this.agent.vaultAddress} confirmed redemption payment for redemption ${redemption.requestId} with proof ${JSON.stringify(web3DeepNormalize(proof))}.`);
@@ -528,7 +528,7 @@ export class AgentBotRedemption {
528528
if (await this.bot.enoughTimePassedToObtainProof(redemption)) {
529529
await this.notifier.sendRedemptionNoProofObtained(redemption.requestId, redemption.proofRequestRound, redemption.proofRequestData);
530530
logger.info(`Agent ${this.agent.vaultAddress} will retry obtaining proof of payment for redemption ${redemption.requestId}.`);
531-
redemption = await this.updateRedemption(rootEm, redemption, {
531+
redemption = await this.updateRedemption(rootEm, redemption, AgentRedemptionState.REQUESTED_PROOF, {
532532
state: AgentRedemptionState.PAID,
533533
proofRequestRound: undefined,
534534
proofRequestData: undefined,
@@ -539,10 +539,14 @@ export class AgentBotRedemption {
539539

540540
/**
541541
* Load and update redemption object in its own transaction.
542+
* If expectedState is not null, it checks in the same transaction that redemption state matches expectedState.
542543
*/
543-
async updateRedemption(rootEm: EM, rd: RedemptionId, modifications: Partial<AgentRedemption>): Promise<AgentRedemption> {
544+
async updateRedemption(rootEm: EM, rd: RedemptionId, expectedState: AgentRedemptionState | null, modifications: Partial<AgentRedemption>): Promise<AgentRedemption> {
544545
return await this.bot.runInTransaction(rootEm, async (em) => {
545546
const redemption = await this.findRedemption(em, rd, LockMode.PESSIMISTIC_WRITE);
547+
if (expectedState != null && redemption.state !== expectedState) {
548+
throw new Error(`Expected redemption ${redemption.requestId} to be in state ${expectedState}, but it is in state ${redemption.state}`);
549+
}
546550
Object.assign(redemption, modifications);
547551
return redemption;
548552
});

packages/fasset-bots-core/test-hardhat/integration/agentBot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ describe("Agent bot tests", () => {
498498
assert.equal(redemptionDone.finalState, AgentRedemptionFinalState.EXPIRED_UNPAID);
499499
});
500500

501-
it("Should perform redemption - agent pays, time expires in indexer: DELETE/REWRITE", async () => {
501+
it("Should perform redemption", async () => {
502502
// perform minting
503503
const crt = await minter.reserveCollateral(agentBot.agent.vaultAddress, 2);
504504
await agentBot.runStep(orm.em);

packages/fasset-bots-core/test-hardhat/unit/actors/agentBot.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,31 @@ describe("Agent bot unit tests", () => {
164164
rd.redeemerAddress = "";
165165
await orm.em.persistAndFlush(rd);
166166
await updateAgentBotUnderlyingBlockProof(context, agentBot);
167-
await agentBot.redemption.handleOpenRedemption(orm.em, rd.state, rd);
167+
await agentBot.redemption.handleActiveRedemptionInState(orm.em, rd.state, rd);
168168
expect(spyLog).to.have.been.called.once;
169169
});
170170

171+
it("Should fail updating redemption state when current state is not correct", async () => {
172+
const agentBot = await createTestAgentBot(context, governance, orm, ownerAddress, ownerUnderlyingAddress, false);
173+
const spyLog = spy.on(console, "error");
174+
// create redemption with invalid state
175+
const rd = new AgentRedemption();
176+
rd.state = AgentRedemptionState.PAID;
177+
rd.agentAddress = "";
178+
rd.requestId = toBN("");
179+
rd.paymentAddress = ""
180+
rd.valueUBA = toBN(0);
181+
rd.feeUBA = toBN(0);
182+
rd.paymentReference = "";
183+
rd.lastUnderlyingBlock = toBN(0);
184+
rd.lastUnderlyingTimestamp = toBN(0);
185+
rd.redeemerAddress = "";
186+
await orm.em.persistAndFlush(rd);
187+
await updateAgentBotUnderlyingBlockProof(context, agentBot);
188+
await expect(agentBot.redemption.payRedemption(orm.em, rd))
189+
.eventually.rejectedWith(/Expected redemption \d+ to be in state started, but it is in state paid/);
190+
});
191+
171192
it("Should not do next minting step due to invalid minting state", async () => {
172193
const agentBot = await createTestAgentBot(context, governance, orm, ownerAddress, ownerUnderlyingAddress, false);
173194
const spyLog = spy.on(console, "error");

0 commit comments

Comments
 (0)