Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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,354 changes: 2,327 additions & 27 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"ts:check": "echo 'No TS type check step'"
},
"dependencies": {
"@nexusmutual/deployments": "^3.0.0",
"@nexusmutual/deployments": "^3.1.0",
"@nexusmutual/ethers-v5-aws-kms-signer": "^0.0.1",
"@nexusmutual/utils": "^0.0.1",
"convict": "^6.2.4",
"dotenv": "^16.0.3",
Expand Down
20 changes: 20 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ const config = convict({
format: Object,
default: {},
},
awsAccessKeyId: {
doc: 'The AWS credentials key ID',
default: '',
env: 'AWS_ACCESS_KEY_ID',
},
awsSecretAccessKey: {
doc: 'The AWS credentials secret access key',
default: '',
env: 'AWS_SECRET_ACCESS_KEY',
},
awsKmsKeyId: {
doc: 'The UUID of the AWS KMS key or AWS KMS key alias',
default: '',
env: 'AWS_KMS_KEY_ID',
},
awsRegion: {
doc: 'The AWS region where the KMS key is located',
default: '',
env: 'AWS_REGION',
},
Comment on lines +36 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Config schema keys =="
rg -nP '^\s*[A-Za-z0-9_]+:\s*\{' src/config.js

echo
echo "== provider key lookups =="
rg -nP "config\.get\('provider'\)|config\.get\('providerUrl'\)" src

Repository: NexusMutual/cover-router

Length of output: 520


Add missing providerUrl config key to prevent RI signing failures.

src/lib/signQuote.js (line 33) reads config.get('providerUrl'), but the config schema at src/config.js only declares provider (line 20). In strict config mode, this causes a failure on the first RI signing request.

Fix
   provider: {
     doc: 'Providers URL',
     env: 'PROVIDER_URL',
     default: '',
   },
+  providerUrl: {
+    doc: 'Providers URL (alias used by RI signer)',
+    env: 'PROVIDER_URL',
+    default: '',
+  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config.js` around lines 36 - 55, The config schema is missing the
providerUrl key referenced by config.get('providerUrl') in signQuote.js; add a
new config entry named providerUrl to the exported config object (matching the
style of awsRegion/awsKmsKeyId entries) with a doc string like "The provider
base URL", a default of '' and an env name such as 'PROVIDER_URL' so strict
config mode no longer errors when signQuote.js calls config.get('providerUrl').

});

// Automatically detect and add PRIORITY_POOLS_ORDER environment variables
Expand Down
10 changes: 7 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const config = require('./config');
const createChainApi = require('./lib/chainApi');
const contractFactory = require('./lib/contracts');
const createEventsApi = require('./lib/eventsApi');
const riContractFactory = require('./lib/riContracts');
const swaggerSpec = require('./lib/swagger');
const createSynchronizer = require('./lib/synchronizer');
const { capacityRouter, pricingRouter, quoteRouter, reindexRouter } = require('./routes');
Expand All @@ -26,11 +27,12 @@ const main = async () => {
provider.pollingInterval = config.get('pollingInterval');

// contract factory
const contracts = await contractFactory(addresses, provider);
const contracts = contractFactory(addresses, provider);
const riContracts = riContractFactory(provider);

// apis
const chainApi = await createChainApi(contracts);
const eventsApi = await createEventsApi(provider, contracts);
const chainApi = await createChainApi(contracts, riContracts);
const eventsApi = await createEventsApi(provider, contracts, riContracts);

const app = express();

Expand Down Expand Up @@ -72,6 +74,7 @@ const main = async () => {
if (!isFromCache) {
console.warn('Missing initial state, delaying startup until the state is fully loaded');
await synchronizer.updateAssetRates();
await synchronizer.updateRiData();
await synchronizer.updateAll();
}

Expand All @@ -83,6 +86,7 @@ const main = async () => {

if (isFromCache) {
await synchronizer.updateAssetRates();
await synchronizer.updateRiData();
await synchronizer.updateAll();
}
};
Expand Down
47 changes: 43 additions & 4 deletions src/lib/capacityEngine.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { ethers, BigNumber } = require('ethers');

const { MAX_COVER_PERIOD } = require('./constants');
const { MAX_COVER_PERIOD, RI_EPOCH_DURATION } = require('./constants');
const {
bnMax,
calculateTrancheId,
Expand All @@ -9,7 +9,14 @@ const {
getCapacitiesInAssets,
getLatestCover,
} = require('./helpers');
const { selectProduct, selectProductPools, selectProductsInPool } = require('../store/selectors');
const {
selectProduct,
selectProductPools,
selectProductsInPool,
selectVaultProducts,
selectRiAssetRate,
selectVaultEpochExpiryTimestamp,
} = require('../store/selectors');

const { WeiPerEther, Zero } = ethers.constants;

Expand Down Expand Up @@ -60,7 +67,7 @@ function calculatePoolUtilizationRate(products) {
function calculateProductCapacity(
store,
productId,
{ poolId = null, period, now, assets, assetRates, withPools = true, editedCover = null },
{ poolId = null, vaultId = null, period, now, assets, assetRates, withPools = true, editedCover = null },
) {
const product = selectProduct(store, productId);
if (!product) {
Expand Down Expand Up @@ -120,10 +127,42 @@ function calculateProductCapacity(
}
}

let totalRiCapacity = Zero;
if (!poolId) {
const expiries = selectVaultEpochExpiryTimestamp(store);
const coverExpiry = now.add(product.gracePeriod).add(period);
const epochDuration = RI_EPOCH_DURATION * 24 * 3600;
const riVaults = selectVaultProducts(store, productId);

totalRiCapacity = riVaults
.filter(vault => vault && expiries[vault.vaultId] && expiries[vault.vaultId].add(epochDuration).gt(coverExpiry))
.reduce((total, vault) => {
const assetRate = selectRiAssetRate(store, vault.asset);
if (!assetRate) {
return total; // Skip vaults without asset rate
}
const allocatedAmount = (vault.allocations || []).reduce((acc, allocation) => {
// cover edit allocation
if (allocation.expiryTimestamp > now && allocation.coverId !== editedCover?.coverId && allocation.active) {
acc = acc.add(allocation.amount);
}
return acc;
}, BigNumber.from(0));
// All values are in vault asset units: activeStake, withdrawalAmount, and allocatedAmount
// Convert to NXM at the end
const availableCapacityInAsset = vault.activeStake.add(vault.withdrawalAmount).sub(allocatedAmount);
const availableCapacityInNXM = availableCapacityInAsset.mul(assetRate).div(WeiPerEther);
return total.add(availableCapacityInNXM);
}, Zero);
}

const { capacityAvailableNXM, capacityUsedNXM, minPrice } = aggregatedData;

// Add RI capacity to pool capacity
const totalCapacityAvailableNXM = capacityAvailableNXM.add(totalRiCapacity);

// The available (i.e. remaining) capacity of a product
const capacityInAssets = getCapacitiesInAssets(capacityAvailableNXM, assets, assetRates);
const capacityInAssets = getCapacitiesInAssets(totalCapacityAvailableNXM, assets, assetRates);

const capacityData = {
productId: Number(productId),
Expand Down
90 changes: 88 additions & 2 deletions src/lib/chainApi.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const { ethers } = require('ethers');
const { BigNumber, ethers } = require('ethers');

const constants = require('./constants');

const { WeiPerEther } = ethers.constants;
const { defaultAbiCoder } = ethers.utils;

const createChainApi = async contracts => {
const createChainApi = async (contracts, riContracts) => {
// contract instances
const cover = contracts('Cover');
const coverProducts = contracts('CoverProducts');
Expand Down Expand Up @@ -108,6 +112,83 @@ const createChainApi = async contracts => {
return packedTrancheAllocation;
};

// RiContracts
const fetchVaultStake = async (vaultId, subnetworks = [], productId = null, riSubnetworks = {}) => {
const operator = '0x51ad1265C8702c9e96Ea61Fe4088C2e22eD4418e';
let maxWeightedStake = BigNumber.from(0);

for (const subnetworkId of subnetworks) {
const subnetworkStake = await riContracts[`delegator_${vaultId}`].stake(subnetworkId, operator);

// Determine the weight to use for this subnetwork
let weight = constants.RI_WEIGHT; // Default weight

if (productId !== null && riSubnetworks && riSubnetworks[subnetworkId]) {
const subnetwork = riSubnetworks[subnetworkId];
// Check if this subnetwork contains the product
if (subnetwork.products && subnetwork.products[String(productId)]) {
weight = subnetwork.products[String(productId)].weight;
}
}

// Calculate weighted stake for this subnetwork: stake * weight / 100
const weightedStake = subnetworkStake.mul(weight).div(constants.RI_WEIGHT_DENOMINATOR);

// Keep track of the maximum weighted stake across all subnetworks
// This allows a subnetwork with lower stake but higher weight to win
maxWeightedStake = weightedStake.gt(maxWeightedStake) ? weightedStake : maxWeightedStake;
}

return maxWeightedStake;
};

const fetchVaultWithdrawals = async vaultId => {
console.log(riContracts[`vault_${vaultId}`].address);
const currentEpoch = await riContracts[`vault_${vaultId}`].currentEpoch();
const withdrawalAmount = await riContracts[`vault_${vaultId}`].withdrawals(currentEpoch + 1);
Comment on lines +147 to +148
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In ethers.js v5, can an ethers.BigNumberbe incremented withcurrentEpoch + 1, or must currentEpoch.add(1) be used?

💡 Result:

In ethers.js v5, an ethers.BigNumber cannot be incremented using the + operator like currentEpoch + 1. Instead, the method currentEpoch.add(1) must be used. The official ethers.js v5 documentation lists arithmetic operations as methods on the BigNumber class: bigNumber.add(otherValue), bigNumber.sub(otherValue), etc. There is no mention of operator overloading for +, which JavaScript classes do not support natively. This contrasts with v6, where BigNumber is replaced by native BigInt, which does support +. Migration docs explicitly show v5 using .add(value2) vs v6 using value1 + value2. Examples attempting native JS operations on BigNumber-related values (e.g., formatted units) show + concatenates as strings, confirming no arithmetic operator support.

Citations:


Replace currentEpoch + 1 with currentEpoch.add(1) for BigNumber arithmetic.

In ethers.js v5, BigNumber does not support the + operator for arithmetic. Attempting currentEpoch + 1 will fail or coerce to an unexpected value, causing incorrect epoch queries or runtime failures when connected to the actual contract.

Suggested fix
-    const withdrawalAmount = await riContracts[`vault_${vaultId}`].withdrawals(currentEpoch + 1);
+    const withdrawalAmount = await riContracts[`vault_${vaultId}`].withdrawals(currentEpoch.add(1));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentEpoch = await riContracts[`vault_${vaultId}`].currentEpoch();
const withdrawalAmount = await riContracts[`vault_${vaultId}`].withdrawals(currentEpoch + 1);
const currentEpoch = await riContracts[`vault_${vaultId}`].currentEpoch();
const withdrawalAmount = await riContracts[`vault_${vaultId}`].withdrawals(currentEpoch.add(1));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/chainApi.js` around lines 147 - 148, The code uses JavaScript `+` on
an ethers BigNumber (`currentEpoch + 1`) which is invalid; update the call that
fetches withdrawals on the vault
(riContracts[`vault_${vaultId}`].withdrawals(...)) to use BigNumber arithmetic,
e.g., call `currentEpoch.add(1)` (or `currentEpoch.add(BigNumber.from(1))`)
instead of `currentEpoch + 1` so the epoch argument is a correct BigNumber.

return withdrawalAmount.mul(constants.RI_WEIGHT).div(constants.RI_WEIGHT_DENOMINATOR);
};

const fetchVaultAllocations = async blockNumber => {
const events = await cover.queryFilter(cover.filters.CoverRiAllocated(), blockNumber);

const allocations = {};
for (const event of events) {
const { args } = event;
const { coverId, data, dataFormat } = args;

const { start, period, productId, originalCoverId } = await fetchCover(coverId);
const coverAllocations = defaultAbiCoder.decode([constants.RI_DATA_FORMATS[dataFormat]], [data]);

Comment on lines +160 to +162
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In ethers.js v5, what does defaultAbiCoder.decode(['tuple(uint256 amount,uint256 vaultId,uint256 subnetworkId,uint256 providerId)[]'], data)return, and shoulddatabe passed directly or wrapped in[data]?

💡 Result:

In ethers.js v5, defaultAbiCoder.decode(['tuple(uint256 amount,uint256 vaultId,uint256 subnetworkId,uint256 providerId)[]'], data) returns a Result object, which is an array subclass containing a single element: an array of arrays. Each inner array represents one tuple (struct) with four elements corresponding to amount, vaultId, subnetworkId, and providerId (as BigNumber objects). If the ABI types had names, the Result would also provide named access (e.g., result[0][0].amount), but since the provided type string lacks names, access is positional only. The data should be passed directly as a hex string (e.g., '0x...'), not wrapped in [data]. The decode method expects the raw ABI-encoded bytes for the specified types; wrapping it in an array would treat it as an encoded array containing that data, which is incorrect for a single top-level array return value. Examples from docs show decode returns nested arrays for tuple and array types, e.g.: - decode(["uint[]", "string"], data) returns [ [1234n, 5678n], "Hello World" ] - decode(["uint", "tuple(uint256, string)"], data) returns [ 1234n, [ 5678n, "Hello World" ] ] For tuple(...)[], it analogously returns [ [ [amt0, vault0, sub0, prov0], [amt1, vault1, sub1, prov1], ... ] ]. Use result[0] to access the array of tuples.

Citations:


🏁 Script executed:

fd -t f chainApi.js

Repository: NexusMutual/cover-router

Length of output: 110


🏁 Script executed:

cat -n src/lib/chainApi.js | sed -n '155,170p'

Repository: NexusMutual/cover-router

Length of output: 816


🏁 Script executed:

rg -n "decodeRiData" --type js

Repository: NexusMutual/cover-router

Length of output: 372


🏁 Script executed:

rg -n "coverAllocations" src/lib/chainApi.js -A 3 -B 1

Repository: NexusMutual/cover-router

Length of output: 506


🏁 Script executed:

rg -n "defaultAbiCoder.decode" src/lib/chainApi.js -A 2 -B 2

Repository: NexusMutual/cover-router

Length of output: 336


🏁 Script executed:

cat -n src/lib/helpers.js | sed -n '300,325p'

Repository: NexusMutual/cover-router

Length of output: 868


🏁 Script executed:

rg -n "RI_DATA_FORMATS" --type js -B 2 -A 5

Repository: NexusMutual/cover-router

Length of output: 1748


🏁 Script executed:

cat -n src/lib/synchronizer.js | sed -n '130,140p'

Repository: NexusMutual/cover-router

Length of output: 683


🏁 Script executed:

cat -n src/lib/constants.js | sed -n '60,65p'

Repository: NexusMutual/cover-router

Length of output: 275


🏁 Script executed:

rg -n "coverAllocations\|allocations" src/lib/chainApi.js -A 2 -B 1 | head -40

Repository: NexusMutual/cover-router

Length of output: 50


🏁 Script executed:

fd -t f "test.*\.js" | head -10

Repository: NexusMutual/cover-router

Length of output: 50


🏁 Script executed:

rg -n "decodeRiData\|RI_DATA_FORMATS" test/ --type js

Repository: NexusMutual/cover-router

Length of output: 50


Fix defaultAbiCoder.decode call: pass data directly and unwrap the Result.

The encode bytes should be passed directly as data, not wrapped in [data]. Since decode with a single-element type array returns a Result with the value at index 0, unwrap it with destructuring. The same bug exists in decodeRiData() in helpers.js.

Suggested fixes

In chainApi.js (line 161):

-      const coverAllocations = defaultAbiCoder.decode([constants.RI_DATA_FORMATS[dataFormat]], [data]);
+      const [coverAllocations] = defaultAbiCoder.decode([constants.RI_DATA_FORMATS[dataFormat]], data);

In helpers.js (line 307):

-const decodeRiData = (data, dataFormat) => {
-  return defaultAbiCoder.decode([RI_DATA_FORMATS[dataFormat]], [data]);
-};
+const decodeRiData = (data, dataFormat) => {
+  const [allocations] = defaultAbiCoder.decode([RI_DATA_FORMATS[dataFormat]], data);
+  return allocations;
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { start, period, productId, originalCoverId } = await fetchCover(coverId);
const coverAllocations = defaultAbiCoder.decode([constants.RI_DATA_FORMATS[dataFormat]], [data]);
const { start, period, productId, originalCoverId } = await fetchCover(coverId);
const [coverAllocations] = defaultAbiCoder.decode([constants.RI_DATA_FORMATS[dataFormat]], data);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/chainApi.js` around lines 160 - 162, The decode call is passing the
encoded bytes wrapped in an array and returning a single-element result; change
the call in the place where you call defaultAbiCoder.decode([...], [data]) (near
where fetchCover is used) to pass data directly as the second argument
(defaultAbiCoder.decode([...], data)) and then unwrap the returned array via
destructuring to get the actual value; apply the same fix in decodeRiData in
helpers.js (use constants.RI_DATA_FORMATS[...] as the types, pass data directly,
and destructure the single-result value).

for (const coverAllocation of coverAllocations) {
const { amount, vaultId } = coverAllocation;
if (!allocations[vaultId]) {
allocations[`${productId}_${vaultId}`] = [];
}

allocations[`${productId}_${vaultId}`].push({
amount,
coverId,
expiryTimestamp: start + period,
originalCoverId,
});
}
}

return allocations;
};

const fetchVaultNextEpochStart = async vaultId => {
return await riContracts[`vault_${vaultId}`].nextEpochStart();
};

const fetchRiAssetRate = async assetId => {
return {
assetRate: await riContracts[`asset_${assetId}`].getRate(),
protocolAssetCorrelationId: riContracts[`asset_${assetId}`].protocolAssetCorrelationId,
};
Comment on lines +185 to +189
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In ethers.js v5, do Solidity public variable getters need to be invoked as functions on a Contractinstance (for examplecontract.foo()vscontract.foo)?

💡 Result:

In ethers.js v5, Solidity public variable getters must be invoked as functions on a Contract instance, using contract.foo rather than contract.foo. Public state variables in Solidity automatically generate view getter functions in the ABI. Ethers.js treats these as regular view functions, requiring the function call syntax with parentheses and await. Examples: - Simple public uint: await contract.totalAllocPoint (not contract.totalAllocPoint) - Public array: await contract.poolInfo(2) - Public mapping: await contract.orderByAssetId(address, id) Direct property access like contract.foo (without ) is not supported and will not trigger the blockchain call. Using Human-Readable ABI requires defining getters explicitly as functions, e.g., 'function totalAllocPoint view returns (uint256)'. This behavior is consistent across ethers.js v5 documentation, GitHub discussions by maintainers, and Stack Exchange answers confirming the getter function invocation.

Citations:


🏁 Script executed:

# Check the actual code at lines 185-189
sed -n '185,189p' src/lib/chainApi.js

Repository: NexusMutual/cover-router

Length of output: 293


🏁 Script executed:

# Search for other potential instances of protocolAssetCorrelationId usage
rg 'protocolAssetCorrelationId' src/lib/chainApi.js -B 2 -A 2

Repository: NexusMutual/cover-router

Length of output: 252


🏁 Script executed:

# Check the context around the function to understand the broader impact
sed -n '180,195p' src/lib/chainApi.js

Repository: NexusMutual/cover-router

Length of output: 501


Invoke protocolAssetCorrelationId() as a function call.

In ethers.js v5, public Solidity state variables automatically generate view getter functions that require function call syntax with parentheses and await. Accessing riContracts[asset_${assetId}].protocolAssetCorrelationId without invoking it returns a function object instead of the correlation ID value, causing the RI asset-rate sync to fail downstream.

Suggested fix
       return {
         assetRate: await riContracts[`asset_${assetId}`].getRate(),
-        protocolAssetCorrelationId: riContracts[`asset_${assetId}`].protocolAssetCorrelationId,
+        protocolAssetCorrelationId: await riContracts[`asset_${assetId}`].protocolAssetCorrelationId(),
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/chainApi.js` around lines 185 - 189, The fetchRiAssetRate function is
returning the protocolAssetCorrelationId function object instead of its value;
change the access of riContracts[`asset_${assetId}`].protocolAssetCorrelationId
to invoke and await the getter (i.e., await
riContracts[`asset_${assetId}`].protocolAssetCorrelationId()) so the actual
correlation ID value is returned alongside the awaited getRate() call.

};

return {
fetchProducts,
fetchProduct,
Expand All @@ -122,6 +203,11 @@ const createChainApi = async contracts => {
fetchCover,
fetchCoverPoolTrancheAllocations,
fetchCoverReference,
fetchVaultStake,
fetchVaultWithdrawals,
fetchVaultAllocations,
fetchVaultNextEpochStart,
fetchRiAssetRate,
};
};

Expand Down
16 changes: 15 additions & 1 deletion src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,21 @@ module.exports = {
SECONDS_PER_DAY: BigNumber.from(24 * 60 * 60),

// set to minimal cover id which is not expired
FETCH_COVER_DATA_FROM_ID: 992,
FETCH_COVER_DATA_FROM_ID: 3710,
Comment on lines 48 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't bootstrap from a moving cover floor.

src/lib/synchronizer.js:88-94 rebuilds the cover scan range from this constant on every updateAll(). Raising it to 3710 means any still-active cover below 3710 can be silently skipped after a restart or cache rebuild. This needs a persisted cursor or a lower bound derived from chain state instead of a date-sensitive constant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/constants.js` around lines 48 - 49, The hardcoded
FETCH_COVER_DATA_FROM_ID constant causes the synchronizer (updateAll in
src/lib/synchronizer.js) to skip still-active covers after restarts; replace
this date-sensitive constant with a persisted cursor or a chain-derived lower
bound: implement a stored cursor (e.g., in DB or file) that updateAll reads and
advances as it scans, falling back to a safe minimal ID or a lower bound
computed from chain state if no cursor exists, and remove/ignore the fixed
FETCH_COVER_DATA_FROM_ID usage so the synchronizer initializes its scan range
from the persisted cursor or chain-derived bound instead.


// Ri constants
SYMBIOTIC_PROVIDER_ID: 1,
RI_FETCH_COVER_DATA_FROM_BLOCK: 24219565,
RI_WEIGHT: 25,
RI_WEIGHT_DENOMINATOR: 100,
RI_THRESHOLD: 20_000_000 * 1_000_000, // in USDC
RI_MIN_COVER_AMOUNT: 100_000 * 1_000_000, // in USDC
RI_COVER_AMOUNT_PERCENTAGE: 80,
RI_COVER_AMOUNT_DENOMINATOR: 100,
RI_EPOCH_DURATION: 70, // days
Comment on lines +54 to +60
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want some of these as env var config?


RI_DATA_FORMATS: {
1: 'tuple(uint256 amount,uint256 vaultId, uint256 subnetworkId,uint256 providerId)[]',
},
HTTP_STATUS,
};
41 changes: 37 additions & 4 deletions src/lib/eventsApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { calculateTrancheId, calculateBucketId } = require('./helpers');

const events = ['StakeBurned', 'DepositExtended', 'StakeDeposited', 'PoolFeeChanged', 'Deallocated'];

module.exports = async (provider, contracts) => {
module.exports = async (provider, contracts, riContracts) => {
// event emitter
const emitter = new EventEmitter();

Expand All @@ -24,12 +24,12 @@ module.exports = async (provider, contracts) => {

// emit an event on every block
provider.on('block', async blockNumber => {
const { timestamp: blockTimestamp } = await provider.getBlock(blockNumber);
const now = Math.floor(Date.now() / 1000);
const activeBucketId = calculateBucketId(now);
const activeTrancheId = calculateTrancheId(now);

if (activeBucketId !== currentBucketId) {
const { timestamp: blockTimestamp } = await provider.getBlock(blockNumber);
const blockBucketId = calculateBucketId(blockTimestamp);

if (blockBucketId === activeBucketId) {
Expand All @@ -41,7 +41,6 @@ module.exports = async (provider, contracts) => {
}

if (activeTrancheId !== currentTrancheId) {
const { timestamp: blockTimestamp } = await provider.getBlock(blockNumber);
const blockTrancheId = calculateTrancheId(blockTimestamp);

if (blockTrancheId === activeTrancheId) {
Expand All @@ -52,7 +51,7 @@ module.exports = async (provider, contracts) => {
}
}

emitter.emit('block', blockNumber);
emitter.emit('block', blockNumber, blockTimestamp);
});

// listeners
Expand Down Expand Up @@ -104,6 +103,40 @@ module.exports = async (provider, contracts) => {
emitter.emit('cover:change', coverId);
});

// Cover Ri events
cover.on('CoverRiAllocated', (coverId, premium, paymentAsset, data, dataFormat) => {
console.info(`Event: Cover ${coverId} allocated with RI`);
emitter.emit('ri:bought', coverId, data, dataFormat);
});

for (const contractName of Object.keys(riContracts)) {
const vaultId = contractName.split('_')[1];
if (contractName.startsWith('vault_')) {
riContracts[contractName].on('Withdraw', () => {
console.info(`Event: Withdraw for vault ${vaultId}`);
emitter.emit('ri:withdraw', vaultId);
});
riContracts[`vault_${vaultId}`].on('Deposit', () => {
console.info(`Event: Deposit for vault ${vaultId}`);
emitter.emit('ri:deposit', vaultId);
});
riContracts[`vault_${vaultId}`].on('OnSlash', () => {
console.info(`Event: Slash for vault ${vaultId}`);
emitter.emit('ri:slash', vaultId);
});
}
if (contractName.startsWith('delegator_')) {
riContracts[`delegator_${vaultId}`].on('SetMaxNetworkLimit', () => {
console.info(`Event: SetMaxNetworkLimit for vault ${vaultId}`);
emitter.emit('ri:setMaxNetworkLimit', vaultId);
});
riContracts[`delegator_${vaultId}`].on('SetNetworkLimit', () => {
console.info(`Event: SetNetworkLimit for vault ${vaultId}`);
emitter.emit('ri:setNetworkLimit', vaultId);
});
}
}

return {
on: emitter.on.bind(emitter),
off: emitter.off.bind(emitter),
Expand Down
7 changes: 7 additions & 0 deletions src/lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ const {
CAPACITY_BUFFER_MINIMUM,
MAX_ACTIVE_TRANCHES,
HTTP_STATUS,
RI_DATA_FORMATS,
} = require('./constants');
const { ApiError } = require('./error');
const { selectCover } = require('../store/selectors');

const { BigNumber } = ethers;
const { WeiPerEther, Zero } = ethers.constants;
const { defaultAbiCoder } = ethers.utils;

/* Bignumber Utils */

Expand Down Expand Up @@ -301,6 +303,10 @@ function getLatestCover(store, originalCoverId) {
: selectCover(store, originalCover.latestCoverId);
}

const decodeRiData = (data, dataFormat) => {
return defaultAbiCoder.decode([RI_DATA_FORMATS[dataFormat]], [data]);
};
Comment on lines +306 to +308
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In ethers.js v5.x, what is the exact signature of ethers.utils.defaultAbiCoder.decode, and should the second argument be a BytesLike value or an array containing the encoded payload?

💡 Result:

In ethers.js v5.x, the exact signature of ethers.utils.defaultAbiCoder.decode is abiCoder.decode(types, data) => Result, where 'types' is an array of strings or ParamType objects describing the expected output types, and 'data' is a single BytesLike value (such as a hex string prefixed with '0x' containing the encoded ABI data). The second argument should be a BytesLike value, not an array containing the encoded payload. All examples in the official documentation pass a single hex string as the second parameter, such as ethers.utils.defaultAbiCoder.decode(["uint", "string"], "0x000000...").

Citations:


Fix the defaultAbiCoder.decode call.

The ethers.utils.defaultAbiCoder.decode method expects the encoded payload as a BytesLike value (second argument), not an array. Passing [data] will cause a runtime error when this helper is called.

Suggested fix
 const decodeRiData = (data, dataFormat) => {
-  return defaultAbiCoder.decode([RI_DATA_FORMATS[dataFormat]], [data]);
+  const type = RI_DATA_FORMATS[dataFormat];
+  if (!type) {
+    throw new Error(`Unsupported RI data format: ${dataFormat}`);
+  }
+  return defaultAbiCoder.decode([type], data);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const decodeRiData = (data, dataFormat) => {
return defaultAbiCoder.decode([RI_DATA_FORMATS[dataFormat]], [data]);
};
const decodeRiData = (data, dataFormat) => {
const type = RI_DATA_FORMATS[dataFormat];
if (!type) {
throw new Error(`Unsupported RI data format: ${dataFormat}`);
}
return defaultAbiCoder.decode([type], data);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/helpers.js` around lines 306 - 308, The decodeRiData helper wrongly
passes the encoded payload as an array; update decodeRiData to pass the raw
BytesLike value (not [data]) into defaultAbiCoder.decode using the ABI type from
RI_DATA_FORMATS (and if necessary normalize the input to a BytesLike via
ethers.utils.arrayify or a string check) so call
defaultAbiCoder.decode([RI_DATA_FORMATS[dataFormat]], data) (or arrayify(data)
when required) inside the decodeRiData function.


module.exports = {
bnMax,
bnMin,
Expand All @@ -319,4 +325,5 @@ module.exports = {
getCoverTrancheAllocations,
calculateCoverRefundInNXM,
getLatestCover,
decodeRiData,
};
Loading
Loading