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
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ class FollowAmuletConversionRateFeedTimeBasedIntegrationTest
loggerFactory.assertLogs(
runTrigger,
_.warningMessage should include(
"200.0000000000 which is outside of the configured accepted range"
"200.0000000000 which is outside of the configured accepted range RangeConfig(0.01,100.0), clamping to 100.0"
),
)
BigDecimal(
sv1Backend.listAmuletPriceVotes().loneElement.payload.amuletPrice.toScala.value
) shouldBe BigDecimal(23.0)
) shouldBe BigDecimal(100.0)
},
)
// Advance below the configured bound
Expand Down Expand Up @@ -206,12 +206,12 @@ class FollowAmuletConversionRateFeedTimeBasedIntegrationTest
loggerFactory.assertLogs(
runTrigger,
_.warningMessage should include(
"0.0010000000 which is outside of the configured accepted range"
"0.0010000000 which is outside of the configured accepted range RangeConfig(0.01,100.0), clamping to 0.01"
),
)
BigDecimal(
sv1Backend.listAmuletPriceVotes().loneElement.payload.amuletPrice.toScala.value
) shouldBe BigDecimal(23.0)
) shouldBe BigDecimal(0.01)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,44 +51,49 @@ class FollowAmuletConversionRateFeedTrigger(
logger.warn(s"No AmuletConversionRateFeed for publisher ${config.publisher}")
Future.successful(Seq.empty)
case Some(feed) =>
val feedRate = BigDecimal(feed.payload.amuletConversionRate)
if (feedRate < config.acceptedRange.min || feedRate > config.acceptedRange.max) {
logger.warn(
s"Rate from publisher ${config.publisher} is ${feedRate} which is outside of the configured accepted range ${config.acceptedRange}"
)
Future.successful(Seq.empty)
} else {
for {
existingVote <- store
.lookupAmuletPriceVoteByThisSv()
.map(
_.getOrElse(
// This can happen after a hard migration or when reingesting from
// ledger begin for other reasons so we don't make this INTERNAL.
throw Status.NOT_FOUND
.withDescription("No price vote for this SV found")
.asRuntimeException
)
val followedFeedRate = BigDecimal(feed.payload.amuletConversionRate)
val publishedFeedRate =
if (
followedFeedRate < config.acceptedRange.min || followedFeedRate > config.acceptedRange.max
) {
val rate =
followedFeedRate.max(config.acceptedRange.min).min(config.acceptedRange.max)
logger.warn(
s"Rate from publisher ${config.publisher} is ${followedFeedRate} which is outside of the configured accepted range ${config.acceptedRange}, clamping to ${rate}"
)
rate
} else followedFeedRate
for {
existingVote <- store
.lookupAmuletPriceVoteByThisSv()
.map(
_.getOrElse(
// This can happen after a hard migration or when reingesting from
// ledger begin for other reasons so we don't make this INTERNAL.
throw Status.NOT_FOUND
.withDescription("No price vote for this SV found")
.asRuntimeException
)
dsoRules <- store.getDsoRules()
voteCooldown = dsoRules.contract.payload.config.voteCooldownTime.toScala
.fold(Duration.ofMinutes(1))(t => Duration.ofNanos(t.microseconds * 1000))
} yield {
val earliestVoteTimestamp = CantonTimestamp
.tryFromInstant(existingVote.payload.lastUpdatedAt.plus(voteCooldown))
if (
earliestVoteTimestamp < now && existingVote.payload.amuletPrice.toScala
.map(BigDecimal(_)) != Some(feedRate)
) {
Seq(
Task(
existingVote,
feed,
)
)
dsoRules <- store.getDsoRules()
voteCooldown = dsoRules.contract.payload.config.voteCooldownTime.toScala
.fold(Duration.ofMinutes(1))(t => Duration.ofNanos(t.microseconds * 1000))
} yield {
val earliestVoteTimestamp = CantonTimestamp
.tryFromInstant(existingVote.payload.lastUpdatedAt.plus(voteCooldown))
if (
earliestVoteTimestamp < now && existingVote.payload.amuletPrice.toScala
.map(BigDecimal(_)) != Some(publishedFeedRate)
) {
Seq(
Task(
existingVote,
feed,
publishedFeedRate,
)
} else {
Seq.empty
}
)
} else {
Seq.empty
}
}
}
Expand All @@ -103,15 +108,15 @@ class FollowAmuletConversionRateFeedTrigger(
_.exerciseDsoRules_UpdateAmuletPriceVote(
store.key.svParty.toProtoPrimitive,
task.work.existingVote.contractId,
task.work.feed.payload.amuletConversionRate,
task.work.publishedFeedRate.bigDecimal,
)
)
_ <- connection
.submit(Seq(store.key.svParty), Seq(store.key.dsoParty), cmd)
.noDedup
.yieldResult()
} yield TaskSuccess(
s"Updated amulet conversion rate to ${task.work.feed.payload.amuletConversionRate}"
s"Updated amulet conversion rate to ${task.work.publishedFeedRate}"
)

override protected def isStaleTask(
Expand All @@ -125,12 +130,15 @@ class FollowAmuletConversionRateFeedTrigger(
object FollowAmuletConversionRateFeedTrigger {
final case class Task(
existingVote: Contract[AmuletPriceVote.ContractId, AmuletPriceVote],
// mainly included for logging, the actual value to publish is publishedFeedRate which can be clamped to the boundaries.
feed: Contract[AmuletConversionRateFeed.ContractId, AmuletConversionRateFeed],
publishedFeedRate: BigDecimal,
) extends PrettyPrinting {

override protected def pretty: Pretty[this.type] = prettyOfClass(
param("existingVote", _.existingVote),
param("feed", _.feed),
param("publishedFeedRate", _.publishedFeedRate),
)
}
}
5 changes: 5 additions & 0 deletions docs/src/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ Upcoming

- Changed the behavior of automation around rewards and coupons to run for the first time in the interval of ``round open time`` -> ``round open time + tick duration``. This might increase the observed duration between rewards and coupons being issued and until they are collected. Once the first tick elapses retries will happen more aggressively.

- SV app

- Published conversion rates are now clamped to the configured range and the clamped value is published instead of
only logging a warning and not publishing an updated value for out of range values.

0.4.20
------

Expand Down
3 changes: 1 addition & 2 deletions docs/src/sv_operator/sv_helm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1134,8 +1134,7 @@ This will automatically pick up the conversion rate from
``#splice-amulet-name-service:Splice.Ans.AmuletConversionRateFeed:AmuletConversionRateFeed``
contracts published by the party ``publisher::namespace`` and set the
SV's config to the latest rate from the publisher. If the published
rate falls outside of the accepted range, a warning is logged and no
change to the SV's published conversion rate is made.
rate falls outside of the accepted range, a warning is logged and the published rate is clamped to the configured range.

Note that SVs must wait ``voteCooldownTime`` (a governance parameter
that defaults to 1min) between updates to their rate. Therefore updates made
Expand Down
Loading