templates.price_feed

   1import smartpy as sp
   2
   3from smartpy.templates import fa2_lib as FA2, price_feed_multisign_admin as MSA
   4
   5main = FA2.main
   6
   7
   8@sp.module
   9def m():
  10    # Constants
  11
  12    NAME = "Price Feed Aggregator"
  13    DESCRIPTION = "XTZ/EUR"
  14    VERSION = "1"
  15    RESERVE_ROUNDS = 2
  16    LINK_TOKEN_ID = 0
  17    DECIMALS = 8
  18    TIMEOUT = 10  # In minutes
  19    ORACLE_PAYMENT = 1
  20    MAX_ROUND = 4294967295  # 2**32-1
  21    USE_BIGMAP_ORACLES = False
  22
  23    # TODO: activate when metadata is implemented
  24    # AGGREGATOR_METADATA = {
  25    #     "name"          : NAME,
  26    #     "version"       : VERSION,
  27    #     "description"   : DESCRIPTION,
  28    #     "source"        : {
  29    #         "tools": [ "SmartPy" ]
  30    #     },
  31    #     "interfaces"    : [ "TZIP-016" ],
  32    # }
  33
  34    # Types
  35
  36    # Round specification type
  37    t_roundData: type = sp.record(
  38        roundId=sp.nat,
  39        answer=sp.nat,
  40        startedAt=sp.timestamp,
  41        updatedAt=sp.timestamp,
  42        answeredInRound=sp.nat,
  43    )
  44    # Round details specification type
  45    t_roundDetails: type = sp.record(
  46        submissions=sp.map[sp.address, sp.nat],
  47        minSubmissions=sp.nat,
  48        maxSubmissions=sp.nat,
  49        timeout=sp.nat,
  50        activeOracles=sp.set[sp.address],
  51    )
  52    # Oracle details specification type
  53    t_oracleDetails: type = sp.record(
  54        startingRound=sp.nat,
  55        endingRound=sp.nat,
  56        lastStartedRound=sp.nat,
  57        withdrawable=sp.nat,
  58        adminAddress=sp.address,
  59    )
  60    # Link token recorded funds specification type
  61    t_recordedFunds: type = sp.record(available=sp.nat, allocated=sp.nat)
  62    # Proxy Admin Action specification type
  63    t_proxyAdminAction: type = sp.variant(
  64        changeActive=sp.bool, changeAdmin=sp.address, changeAggregator=sp.address
  65    )
  66    # Aggregator Admin Action specification type
  67    t_aggregatorAdminAction: type = sp.variant(
  68        changeOracles=sp.record(
  69            removed=sp.list[sp.address],
  70            added=sp.list[
  71                sp.pair[
  72                    sp.address,
  73                    sp.record(
  74                        startingRound=sp.nat,
  75                        endingRound=sp.option[sp.nat],
  76                        adminAddress=sp.address,
  77                    ),
  78                ]
  79            ],
  80        ),
  81        changeActive=sp.bool,
  82        changeAdmin=sp.address,
  83        updateFutureRounds=sp.record(
  84            minSubmissions=sp.nat,
  85            maxSubmissions=sp.nat,
  86            restartDelay=sp.nat,
  87            timeout=sp.nat,
  88            oraclePayment=sp.nat,
  89        ),
  90    )
  91
  92    t_storage_type: type = sp.record(
  93        active=sp.bool,
  94        decimals=sp.nat,
  95        admin=sp.address,
  96        metadata=sp.big_map[sp.string, sp.bytes],
  97        minSubmissions=sp.nat,
  98        maxSubmissions=sp.nat,
  99        restartDelay=sp.nat,
 100        timeout=sp.nat,  # In minutes
 101        oraclePayment=sp.nat,
 102        latestRoundId=sp.nat,
 103        reportingRoundId=sp.nat,
 104        rounds=sp.big_map[sp.nat, t_roundData],
 105        previousRoundDetails=t_roundDetails,
 106        reportingRoundDetails=t_roundDetails,
 107        linkToken=sp.address,
 108        recordedFunds=t_recordedFunds,
 109        oraclesAddresses=sp.set[sp.address],
 110        oracles=sp.big_map[sp.address, t_oracleDetails],
 111    )
 112
 113    t_fa2_balance_of_request: type = sp.record(
 114        owner=sp.address, token_id=sp.nat
 115    ).layout(("owner", "token_id"))
 116
 117    t_fa2_balance_of_response: type = sp.record(
 118        request=t_fa2_balance_of_request, balance=sp.nat
 119    ).layout(("request", "balance"))
 120
 121    t_fa2_balance_of_params: type = sp.record(
 122        callback=sp.contract[list[t_fa2_balance_of_response]],
 123        requests=list[t_fa2_balance_of_request],
 124    ).layout(("requests", "callback"))
 125
 126    t_fa2_tx: type = sp.record(
 127        to_=sp.address,
 128        token_id=sp.nat,
 129        amount=sp.nat,
 130    ).layout(("to_", ("token_id", "amount")))
 131
 132    t_fa2_transfer_batch: type = sp.record(
 133        from_=sp.address,
 134        txs=list[t_fa2_tx],
 135    ).layout(("from_", "txs"))
 136
 137    t_fa2_transfer_params: type = list[t_fa2_transfer_batch]
 138
 139    Aggregator_NotAdmin = "Aggregator_NotAdmin"
 140    Aggregator_NotOracle = "Aggregator_NotOracle"
 141    Aggregator_NotYetEnabledOracle = "Aggregator_NotYetEnabledOracle"
 142    Aggregator_NotLongerAllowedOracle = "Aggregator_NotLongerAllowedOracle"
 143    Aggregator_InvalidRound = "Aggregator_InvalidRound"
 144    Aggregator_FutureRound = "Aggregator_FutureRound"
 145    Aggregator_RoundNotOver = "Aggregator_RoundNotOver"
 146    Aggregator_PreviousRoundNotOver = "Aggregator_PreviousRoundNotOver"
 147    Aggregator_WaitBeforeInit = "Aggregator_WaitBeforeInit"
 148    Aggregator_AlreadySubmitted = "Aggregator_AlreadySubmittedForThisRound"
 149    Aggregator_SubmittedInCurrent = "Aggregator_SubmittedInCurrent"
 150    Aggregator_CurrentHasValue = "Aggregator_CurrentHasValue"
 151    Aggregator_CallbackNotFound = "Aggregator_CallbackNotFound"
 152    Aggregator_DelayExceedTotal = "Aggregator_DelayExceedTotal"
 153    Aggregator_MaxSubmissions = "Aggregator_RoundMaxSubmissionExceed"
 154    Aggregator_MaxInferiorToMin = "Aggregator_MaxInferiorToMin"
 155    Aggregator_MaxExceedActive = "Aggregator_MaxExceedActive"
 156    Aggregator_OraclePaymentUnderflow = "Aggregator_OraclePaymentUnderflow"
 157    Aggregator_NotOracleAdmin = "Aggregator_NotOracleAdmin"
 158    Aggregator_InsufficientWithdrawableFunds = (
 159        "Aggregator_InsufficientWithdrawableFunds"
 160    )
 161    Aggregator_NotLinkToken = "Aggregator_NotLinkToken"
 162    Aggregator_InvalidTokenInterface = "Aggregator_InvalidTokenkInterface"
 163    Aggregator_MinSubmissionsTooLow = "Aggregator_MinSubmissionsTooLow"
 164    Aggregator_InsufficientFundsForPayment = "Aggregator_InsufficientFundsForPayment"
 165
 166    Proxy_InvalidParametersInLatestRoundDataView = (
 167        "Proxy_InvalidParametersInLatestRoundDataView"
 168    )
 169    Proxy_InvalidParametersInDecimalsView = "Proxy_InvalidParametersInDecimalsView"
 170    Proxy_InvalidParametersInDescriptionView = (
 171        "Proxy_InvalidParametersInDescriptionView"
 172    )
 173    Proxy_InvalidParametersInVersionView = "Proxy_InvalidParametersInVersionView"
 174    Proxy_AggregatorNotConfigured = "Proxy_AggregatorNotConfigured"
 175    Proxy_NotAdmin = "Proxy_NotAdmin"
 176
 177    def median(submissions):
 178        """Returns the sorted middle, or the average of the two middle indexed items if the array has an even number of elements."""
 179        xs = submissions
 180        result = 0
 181        half = sp.len(xs) / 2
 182        hist = {}
 183        average = not (half * 2 != sp.len(xs))
 184        for x in xs:
 185            if hist.contains(x):
 186                hist[x] += 1
 187            else:
 188                hist[x] = 1
 189        i = 0
 190        for x in hist.items():
 191            if average:
 192                if i < half:
 193                    result = x.key
 194                else:
 195                    result += x.key
 196                    result /= 2
 197                    average = False
 198                i += x.value
 199            else:
 200                if i <= half:
 201                    result = x.key
 202                    i += x.value
 203        return result
 204
 205    class PriceAggregator(sp.Contract):
 206        def __init__(
 207            self,
 208            initialRoundDetails,
 209            tokenAddress,
 210            active,
 211            decimals,
 212            admin,
 213            oracles,
 214            timeout,
 215            oraclePayment,
 216            minSubmissions,
 217            maxSubmissions,
 218            restartDelay,
 219            metadata,
 220        ):
 221            """
 222            This contract aggregates off-chain data pushed by oracles.
 223
 224            The submissions are gathered in rounds, with each round aggregating the submissions
 225            for each oracle into a single answer.
 226
 227            The latest aggregated answer is exposed as well as historical answers and their updated at timestamp.
 228
 229            Args:
 230                useBigmapOracles           : Use a big map to store the oracles
 231                initialRoundDetails        : Initial round details
 232                tokenContract (sp.Contract): Token Contract used for payment
 233                tokenAddress (sp.address) : Address of the token used for payments
 234                active (sp.bool)          : Aggregator state
 235                decimals (sp.nat)         : The number of decimals in the answer.
 236                admin (sp.address)        : Admin address, supposely the PriceAggregatorAdmin multisign contract
 237                oracleDetails (TYPES.TOracleDetails) : Parameters of the Oracle
 238                timeout (sp.nat)          : Number of minutes after which a new round can be initiate
 239                oraclePayment (sp.nat)    : Amount of Token payed to Oracle at each Submission
 240                minSubmissions (sp.nat)   : Min submissions' number to be able to update a value
 241                maxSubmissions (sp.nat)   : Max submissions' number to be able to seal a value
 242                restartDelay (sp.nat)     : Number of rounds an Oracle has to wait between 2 round initiate
 243                metadata(sp.big_map of str bytes) : metadata bigmap
 244            """
 245
 246            # TODO: activate when metadata is implemented
 247            # # Generate the metadata representation
 248            # # The generated metadata should be copied and destributed with IPFS
 249            # self.init_metadata("metadata", {
 250            #     **AGGREGATOR_METADATA,
 251            #     "views" : [
 252            #         self.getDecimals,
 253            #         self.getWithdrawablePayment,
 254            #         self.getRoundData
 255            #     ],
 256            # })
 257
 258            sp.cast(self.data, t_storage_type)
 259
 260            self.data.active = active
 261            self.data.decimals = decimals
 262            self.data.admin = admin
 263            self.data.metadata = metadata
 264
 265            self.data.minSubmissions = minSubmissions
 266            self.data.maxSubmissions = maxSubmissions
 267            self.data.restartDelay = restartDelay
 268            self.data.timeout = timeout
 269            self.data.oraclePayment = oraclePayment
 270
 271            self.data.latestRoundId = 0
 272            self.data.reportingRoundId = 0
 273
 274            self.data.rounds = sp.big_map()
 275            self.data.previousRoundDetails = initialRoundDetails
 276            self.data.reportingRoundDetails = initialRoundDetails
 277
 278            self.data.linkToken = tokenAddress
 279            self.data.recordedFunds = sp.record(available=0, allocated=0)
 280
 281            self.data.oracles = sp.big_map()
 282            self.data.oraclesAddresses = set()
 283
 284            for oracle in oracles.items():
 285                self.data.oracles[oracle.key] = oracle.value
 286                self.data.oraclesAddresses.add(oracle.key)
 287
 288        @sp.private(with_storage="read-write")
 289        def updatePrevious(self, submission):
 290            currentRoundId = self.data.reportingRoundId
 291            previousRoundId = sp.as_nat(currentRoundId - 1)
 292            previousRoundDetails = self.data.previousRoundDetails
 293
 294            assert not previousRoundDetails.submissions.contains(
 295                sp.sender
 296            ), Aggregator_SubmittedInCurrent
 297            assert (
 298                sp.len(previousRoundDetails.submissions)
 299                < previousRoundDetails.maxSubmissions
 300            ), Aggregator_MaxSubmissions
 301
 302            previousRoundDetails.submissions[sp.sender] = submission
 303            if (
 304                sp.len(previousRoundDetails.submissions)
 305                >= previousRoundDetails.minSubmissions
 306            ):
 307                self.data.rounds[previousRoundId].answer = median(
 308                    previousRoundDetails.submissions.values()
 309                )
 310                self.data.rounds[previousRoundId].updatedAt = sp.now
 311                self.data.rounds[previousRoundId].answeredInRound = currentRoundId
 312            self.data.previousRoundDetails = previousRoundDetails
 313
 314        @sp.private(with_storage="read-write")
 315        def updateCurrent(self, submission):
 316            currentRoundId = self.data.reportingRoundId
 317            currentRoundDetails = self.data.reportingRoundDetails
 318
 319            assert not currentRoundDetails.submissions.contains(
 320                sp.sender
 321            ), Aggregator_AlreadySubmitted
 322            assert (
 323                sp.len(currentRoundDetails.submissions)
 324                < currentRoundDetails.maxSubmissions
 325            ), Aggregator_MaxSubmissions
 326
 327            currentRoundDetails.submissions[sp.sender] = submission
 328            if (
 329                sp.len(currentRoundDetails.submissions)
 330                >= currentRoundDetails.minSubmissions
 331            ):
 332                self.data.rounds[currentRoundId].answer = median(
 333                    currentRoundDetails.submissions.values()
 334                )
 335                self.data.rounds[currentRoundId].updatedAt = sp.now
 336                self.data.rounds[currentRoundId].answeredInRound = currentRoundId
 337                self.data.latestRoundId = currentRoundId
 338            self.data.reportingRoundDetails = currentRoundDetails
 339
 340        @sp.private(with_storage="read-write")
 341        def updateNext(self, submission):
 342            currentRoundId = self.data.reportingRoundId
 343            nextRoundId = currentRoundId + 1
 344
 345            if currentRoundId > 0:
 346                currentRound = self.data.rounds[currentRoundId]
 347
 348                timeout = sp.add_seconds(
 349                    currentRound.startedAt, sp.to_int(self.data.timeout) * 60
 350                )
 351                roundInit_delay = (
 352                    self.data.oracles[sp.sender].lastStartedRound
 353                    + self.data.restartDelay
 354                )
 355
 356                assert (self.data.oracles[sp.sender].lastStartedRound == 0) or (
 357                    nextRoundId > roundInit_delay
 358                ), Aggregator_WaitBeforeInit
 359                assert (sp.now > timeout) or (
 360                    currentRound.answeredInRound == currentRoundId
 361                ), Aggregator_PreviousRoundNotOver
 362
 363            # If minimum submissions is 1, then include the answer in the round initialization
 364            answer = 0
 365            answeredInRound = 0
 366            if self.data.minSubmissions == 1:
 367                answer = submission
 368                answeredInRound = nextRoundId
 369
 370            self.data.rounds[nextRoundId] = sp.record(
 371                roundId=nextRoundId,
 372                answer=answer,
 373                startedAt=sp.now,
 374                updatedAt=sp.now,
 375                answeredInRound=answeredInRound,
 376            )
 377
 378            self.data.previousRoundDetails = self.data.reportingRoundDetails
 379
 380            activeOracles = set()
 381            for oracle in self.data.oraclesAddresses.elements():
 382                oracleDetails = self.data.oracles[oracle]
 383                if (oracleDetails.endingRound >= nextRoundId) and (
 384                    oracleDetails.startingRound <= nextRoundId
 385                ):
 386                    activeOracles.add(oracle)
 387
 388            self.data.reportingRoundDetails = sp.record(
 389                submissions={sp.sender: submission},
 390                minSubmissions=self.data.minSubmissions,
 391                maxSubmissions=self.data.maxSubmissions,
 392                timeout=self.data.timeout,
 393                activeOracles=activeOracles,
 394            )
 395
 396            self.data.oracles[sp.sender].lastStartedRound = nextRoundId
 397            self.data.reportingRoundId = nextRoundId
 398
 399        @sp.entrypoint
 400        def submit(self, params):
 401            """
 402            Called by oracles when they have witnessed a need to update
 403
 404            Args:
 405                roundId (sp.Nat)     : ID of the round this submission pertains to
 406                submission (sp.nat) : updated data that the oracle is submitting
 407            """
 408            currentRoundId = self.data.reportingRoundId
 409            (roundId, submission) = params
 410
 411            assert self.data.active
 412            assert self.data.oracles.contains(sp.sender), Aggregator_NotOracle
 413            assert (
 414                self.data.oracles[sp.sender].startingRound <= roundId
 415            ), Aggregator_NotYetEnabledOracle
 416            assert (
 417                self.data.oracles[sp.sender].endingRound > roundId
 418            ), Aggregator_NotLongerAllowedOracle
 419
 420            # Only allow new submissions in [currentRoundId -1; currentRoundId +1] round interval
 421            assert (
 422                (roundId + 1 == currentRoundId)
 423                or (roundId == currentRoundId)
 424                or (roundId == currentRoundId + 1)
 425            ), Aggregator_InvalidRound
 426
 427            if (roundId + 1) == currentRoundId:
 428                _ = self.updatePrevious(submission)
 429            else:
 430                if roundId == currentRoundId:
 431                    _ = self.updateCurrent(submission)
 432                else:
 433                    _ = self.updateNext(submission)
 434
 435            # Update the oracle withdrawable amount
 436            _ = self.payOracle(sp.sender)
 437
 438        @sp.entrypoint
 439        def administrate(self, actions):
 440            assert sp.sender == self.data.admin, Aggregator_NotAdmin
 441            sp.cast(actions, sp.list[t_aggregatorAdminAction])
 442            for action in actions:
 443                with sp.match(action):
 444                    with sp.case.changeActive as active:
 445                        self.data.active = active
 446                    with sp.case.changeAdmin as admin:
 447                        self.data.admin = admin
 448                    with sp.case.updateFutureRounds as futureRounds:
 449                        _ = self.updateFutureRounds(futureRounds)
 450                    with sp.case.changeOracles as params:
 451                        _ = self.changeOracles(params)
 452
 453        @sp.private(with_storage="read-write")
 454        def updateFutureRounds(self, params):
 455            assert sp.sender == self.data.admin, Aggregator_NotAdmin
 456
 457            oraclesCount = sp.len(self.data.oraclesAddresses)
 458            assert (
 459                params.maxSubmissions >= params.minSubmissions
 460            ), Aggregator_MaxInferiorToMin
 461            assert oraclesCount >= params.maxSubmissions, Aggregator_MaxExceedActive
 462            assert (oraclesCount == 0) or (
 463                oraclesCount > params.restartDelay
 464            ), Aggregator_DelayExceedTotal
 465            assert (oraclesCount == 0) or (
 466                params.minSubmissions > 0
 467            ), Aggregator_MinSubmissionsTooLow
 468
 469            required_funds = params.oraclePayment * oraclesCount * RESERVE_ROUNDS
 470            assert (
 471                self.data.recordedFunds.available >= required_funds
 472            ), Aggregator_InsufficientFundsForPayment
 473
 474            self.data.restartDelay = params.restartDelay
 475            self.data.minSubmissions = params.minSubmissions
 476            self.data.maxSubmissions = params.maxSubmissions
 477            self.data.timeout = params.timeout
 478            self.data.oraclePayment = params.oraclePayment
 479
 480        @sp.private(with_storage="read-write")
 481        def changeOracles(self, params):
 482            assert sp.sender == self.data.admin, Aggregator_NotAdmin
 483            for oracleAddress in params.removed:
 484                del self.data.oracles[oracleAddress]
 485                self.data.oraclesAddresses.remove(oracleAddress)
 486
 487            for oracle in params.added:
 488                (oracleAddress, oracleData) = oracle
 489                endingRound = MAX_ROUND
 490
 491                if oracleData.endingRound.is_some():
 492                    endingRound = oracleData.endingRound.unwrap_some()
 493
 494                self.data.oracles[oracleAddress] = sp.record(
 495                    startingRound=oracleData.startingRound,
 496                    endingRound=endingRound,
 497                    adminAddress=oracleData.adminAddress,
 498                    lastStartedRound=0,
 499                    withdrawable=sp.nat(0),
 500                )
 501
 502                self.data.oraclesAddresses.add(oracleAddress)
 503
 504                reportingRoundId = self.data.reportingRoundId
 505                if (reportingRoundId != 0) and (
 506                    oracleData.startingRound <= reportingRoundId
 507                ):
 508                    self.data.reportingRoundDetails.activeOracles.add(oracleAddress)
 509
 510        @sp.entrypoint
 511        def decimals(self, callback):
 512            """Returns the number of decimals in the answer (e.g. 8)"""
 513            sp.transfer(self.data.decimals, sp.tez(0), callback)
 514
 515        @sp.entrypoint
 516        def latestRoundData(self, callback):
 517            """
 518            Callback with data about the latest round. Consumers are encouraged
 519            to check that they're receiving fresh data by inspecting the
 520            updatedAt and answeredInRound return values.
 521
 522            Args:
 523                callback : sp.address
 524
 525            Returns:
 526                roundId (sp.nat): round ID for which data was retrieved
 527                answer (sp.nat):  the answer for the given round
 528                startedAt (sp.timestamp): timestamp when the round was started.
 529                updatedAt (sp.timestamp): timestamp when the answer was updated.
 530                    (i.e. answer was last computed)
 531                answeredInRound (sp.nat): the round ID of the round in which
 532                    the answer was computed. answeredInRound may be smaller than
 533                    roundId when the round timed out.
 534                    answeredInRound is equal to roundId when the round didn't
 535                    time out and was computed regularly.
 536
 537                Note that for in-progress rounds (i.e. rounds that haven't yet
 538                received maxSubmissions) answer and updatedAt may change
 539                between queries.
 540            """
 541            sp.transfer(self.data.rounds[self.data.latestRoundId], sp.tez(0), callback)
 542
 543        @sp.offchain_view
 544        def getWithdrawablePayment(self, oracleAddress):
 545            """
 546            Gets the available amount that an oracle can withdraw
 547
 548            Args:
 549                oracleAddress : sp.address
 550
 551            Returns:
 552                sp.nat : Withdrawable payment amount
 553            """
 554            return self.data.oracles[oracleAddress].withdrawable
 555
 556        @sp.offchain_view
 557        def getDecimals(self):
 558            """
 559            Gets the number of decimals in the answer (e.g. 8)
 560
 561            Returns:
 562                sp.nat : the number of decimals
 563            """
 564            return self.data.decimals
 565
 566        @sp.offchain_view
 567        def getRoundData(self, roundId):
 568            """
 569            Gets the information of a specific round
 570
 571            Args:
 572                roundId : sp.nat
 573
 574            Returns:
 575                roundId         (sp.nat)       : round ID for which data was retrieved.
 576                answer          (sp.nat)       : the answer for the given round.
 577                startedAt       (sp.timestamp) : timestamp when the round was started.
 578                updatedAt       (sp.timestamp) : timestamp when the answer was updated.
 579                answeredInRound (sp.nat)       : the round ID of the round in which
 580                    the answer was computed. answeredInRound may be smaller than
 581                    roundId when the round timed out.
 582                    answeredInRound is equal to roundId when the round didn't
 583                    time out and was computed regularly.
 584
 585                Note that for in-progress rounds (i.e. rounds that haven't yet
 586                received maxSubmissions) answer and updatedAt may change
 587                between queries.
 588            """
 589            if self.data.rounds.contains(roundId):
 590                return sp.Some(self.data.rounds[roundId])
 591            else:
 592                return None
 593
 594        @sp.private(with_storage="read-write")
 595        def payOracle(self, oracle):
 596            """
 597            Update the withdrawable amount of a specific oracle after a sucessul submission
 598            """
 599            payment = self.data.oraclePayment
 600            funds = self.data.recordedFunds
 601            funds.available = sp.as_nat(
 602                funds.available - payment, error=Aggregator_OraclePaymentUnderflow
 603            )
 604            funds.allocated += payment
 605            self.data.recordedFunds = funds
 606            self.data.oracles[oracle].withdrawable += payment
 607
 608        @sp.private(with_storage="read-write", with_operations=True)
 609        def requestBalanceUpdate(self, updateAvailableFunds):
 610            """
 611            Call Link token and request a balance update
 612
 613            Args:
 614                updateAvailableFunds (sp.contract): updateAvailableFunds's entrypoint
 615            """
 616            contract = sp.contract(
 617                t_fa2_balance_of_params, self.data.linkToken, entrypoint="balance_of"
 618            ).unwrap_some(error=Aggregator_InvalidTokenInterface)
 619
 620            args = sp.record(
 621                callback=updateAvailableFunds,
 622                requests=[sp.record(owner=sp.self_address(), token_id=LINK_TOKEN_ID)],
 623            )
 624            sp.transfer(args, sp.tez(0), contract)
 625
 626        @sp.entrypoint
 627        def forceBalanceUpdate(self):
 628            """
 629            Call Link token and request a balance update (Forced, this should be called after an origination)
 630            """
 631            _ = self.requestBalanceUpdate(sp.self_entrypoint("updateAvailableFunds"))
 632
 633        @sp.entrypoint
 634        def updateAvailableFunds(self, params):
 635            """
 636            Receive balance update from link token
 637            """
 638            sp.cast(params, sp.list[t_fa2_balance_of_response])
 639
 640            # Ensure that this entrypoint is only called by the configured token
 641            assert sp.sender == self.data.linkToken, Aggregator_NotLinkToken
 642
 643            balance = 0
 644            for resp in params:
 645                assert resp.request.owner == sp.self_address()
 646                balance = resp.balance
 647
 648            if balance != self.data.recordedFunds.available:
 649                self.data.recordedFunds.available = balance
 650
 651        @sp.entrypoint
 652        def withdrawPayment(self, params):
 653            """
 654            Transfers the oracle's LINK to another address. Can only be called by the oracle's admin.
 655
 656            Args:
 657                oracleAddress       : sp.address   is the oracle whose LINK is transferred
 658                recipientAddress    : sp.address   is the address to send the LINK to
 659                amount              : sp.nat       is the amount of LINK to send
 660            """
 661            assert (
 662                self.data.oracles[params.oracleAddress].adminAddress == sp.sender
 663            ), Aggregator_NotOracleAdmin
 664
 665            withdrawable = self.data.oracles[params.oracleAddress].withdrawable
 666
 667            assert (
 668                withdrawable >= params.amount
 669            ), Aggregator_InsufficientWithdrawableFunds
 670
 671            self.data.oracles[params.oracleAddress].withdrawable = sp.as_nat(
 672                withdrawable - params.amount
 673            )
 674            self.data.recordedFunds.allocated = sp.as_nat(
 675                self.data.recordedFunds.allocated - params.amount
 676            )
 677
 678            token = sp.contract(
 679                t_fa2_transfer_params, self.data.linkToken, entrypoint="transfer"
 680            ).unwrap_some(error=Aggregator_InvalidTokenInterface)
 681            arg = [
 682                sp.record(
 683                    from_=sp.self_address(),
 684                    txs=[
 685                        sp.record(
 686                            to_=params.recipientAddress,
 687                            token_id=LINK_TOKEN_ID,
 688                            amount=params.amount,
 689                        )
 690                    ],
 691                )
 692            ]
 693            # Send payment
 694            sp.transfer(arg, sp.tez(0), token)
 695            # Resync available funds
 696            _ = self.requestBalanceUpdate(sp.self_entrypoint("updateAvailableFunds"))
 697
 698    class Proxy(sp.Contract):
 699        def __init__(self, active, admin, aggregator):
 700            sp.cast(
 701                self.data,
 702                sp.record(
 703                    active=sp.bool, admin=sp.address, aggregator=sp.option[sp.address]
 704                ),
 705            )
 706
 707            self.data.active = active
 708            self.data.admin = admin
 709            self.data.aggregator = aggregator
 710
 711        @sp.entrypoint
 712        def decimals(self, params):
 713            aggregator = self.data.aggregator.unwrap_some(
 714                error=Proxy_AggregatorNotConfigured
 715            )
 716            view = sp.contract(
 717                sp.pair[sp.unit, sp.address], aggregator, entrypoint="decimals"
 718            ).unwrap_some(error=Proxy_InvalidParametersInDecimalsView)
 719
 720            sp.transfer(params, sp.tez(0), view)
 721
 722        @sp.entrypoint
 723        def version(self, params):
 724            aggregator = self.data.aggregator.unwrap_some(
 725                error=Proxy_AggregatorNotConfigured
 726            )
 727            view = sp.contract(
 728                sp.pair[sp.unit, sp.address], aggregator, entrypoint="version"
 729            ).unwrap_some(error=Proxy_InvalidParametersInVersionView)
 730
 731            sp.transfer(params, sp.tez(0), view)
 732
 733        @sp.entrypoint
 734        def description(self, params):
 735            aggregator = self.data.aggregator.unwrap_some(
 736                error=Proxy_AggregatorNotConfigured
 737            )
 738            view = sp.contract(
 739                sp.pair[sp.unit, sp.address], aggregator, entrypoint="description"
 740            ).unwrap_some(error=Proxy_InvalidParametersInDescriptionView)
 741            sp.transfer(params, sp.tez(0), view)
 742
 743        @sp.entrypoint
 744        def latestRoundData(self, params):
 745            aggregator = self.data.aggregator.unwrap_some(
 746                error=Proxy_AggregatorNotConfigured
 747            )
 748            view = sp.contract(
 749                sp.pair[sp.unit, sp.address], aggregator, "latestRoundData"
 750            ).unwrap_some(error=Proxy_InvalidParametersInLatestRoundDataView)
 751            sp.transfer(params, sp.tez(0), view)
 752
 753        @sp.entrypoint
 754        def administrate(self, actions):
 755            assert sp.sender == self.data.admin, Proxy_NotAdmin
 756            sp.cast(actions, sp.list[t_proxyAdminAction])
 757            for action in actions:
 758                with sp.match(action):
 759                    with sp.case.changeActive as active:
 760                        self.data.active = active
 761                    with sp.case.changeAdmin as admin:
 762                        self.data.admin = admin
 763                    with sp.case.changeAggregator as aggregator:
 764                        self.data.aggregator = sp.Some(aggregator)
 765
 766    class Viewer(sp.Contract):
 767        def __init__(self, admin, proxy):
 768            self.data.admin = admin
 769            self.data.proxy = proxy
 770            self.data.latestRoundData = None
 771
 772        @sp.entrypoint
 773        def getLatestRoundData(self):
 774            proxy = sp.contract(
 775                sp.address, self.data.proxy, entrypoint="latestRoundData"
 776            ).unwrap_some(
 777                error="Wrong Interface: Could not resolve proxy latestRoundData entrypoint."
 778            )
 779
 780            sp.transfer(
 781                sp.to_address(sp.self_entrypoint("setLatestRoundData")),
 782                sp.tez(0),
 783                proxy,
 784            )
 785
 786        @sp.entrypoint
 787        def setLatestRoundData(self, latestRoundData):
 788            sp.cast(latestRoundData, t_roundData)
 789            assert sp.sender == self.data.proxy
 790            self.data.latestRoundData = sp.Some(latestRoundData)
 791
 792        @sp.entrypoint
 793        def setup(self, admin, proxy):
 794            assert sp.sender == self.data.admin
 795            self.data.admin = admin
 796            self.data.proxy = proxy
 797
 798    # Order of inheritance: [Admin], [<policy>], <base class>, [<mixins>]
 799
 800    class LinkToken(
 801        main.Admin,
 802        main.PauseOwnerOrOperatorTransfer,
 803        main.SingleAsset,
 804        main.ChangeMetadata,
 805        main.WithdrawMutez,
 806        main.MintSingleAsset,
 807        main.BurnSingleAsset,
 808        main.OffchainviewTokenMetadata,
 809        main.OnchainviewBalanceOf,
 810    ):
 811        def __init__(self, administrator, metadata, ledger, token_metadata):
 812            main.OnchainviewBalanceOf.__init__(self)
 813            main.OffchainviewTokenMetadata.__init__(self)
 814            main.BurnSingleAsset.__init__(self)
 815            main.MintSingleAsset.__init__(self)
 816            main.WithdrawMutez.__init__(self)
 817            main.ChangeMetadata.__init__(self)
 818            main.SingleAsset.__init__(self, metadata, ledger, token_metadata)
 819            main.PauseOwnerOrOperatorTransfer.__init__(self)
 820            main.Admin.__init__(self, administrator)
 821
 822
 823# CONSTANTS (Useful when originating the contracts)
 824GENERATE_DEPLOYMENT_CONTRACTS = False
 825
 826ADMIN_ADDRESS = sp.address("tz1evBmfWZoPDN38avoRGbjJaLBJUP8AZz6a")
 827AGGREGATOR_ADDRESS = sp.address("KT1CfuSjCcunNQ5qCCro2Kc74uivnor9d8ba")
 828PROXY_ADDRESS = sp.address("KT1PG6uK91ymZYVtjnRXv2mEdFYSH6P6uJhC")
 829TOKEN_ADDRESS = sp.address("KT1LcrXERzpDeUXWxLEWnLipHrhWEhzSRTt7")
 830
 831ORACLE_1_ADDRESS = sp.address("KT1LhTzYhdhxTqKu7ByJz8KaShF6qPTdx5os")
 832ORACLE_2_ADDRESS = sp.address("KT1P7oeoKWHx5SXt73qpEanzkr8yeEKABqko")
 833ORACLE_3_ADDRESS = sp.address("KT1SCkxmTqTkmc7zoAP5uMYT9rp9iqVVRgdt")
 834ORACLE_4_ADDRESS = sp.address("KT1LLTzYhdhxTqKu7ByJz8KaShF6qPTdx5os")
 835ORACLE_5_ADDRESS = sp.address("KT1P6oeoKWHx5SXt73qpEanzkr8yeEKABqko")
 836ORACLE_6_ADDRESS = sp.address("KT1SskxmTqTkmc7zoAP5uMYT9rp9iqVVRgdt")
 837
 838# Admins/Signers of multisign admin contract
 839ACCOUNT_1_ADDRESS = sp.address("tz1evBmfWZoPDN38avoRGbjJaLBJUP8AZz6a")
 840ACCOUNT_1_PUBLIC_KEY = sp.key("edpkuZ7ERiU5B8knLqQsVMH86j9RLMUyHyL665oCXDkPQxF7HGqSeJ")
 841ACCOUNT_1_SECRET = "edskRowES25ZTKFDV5GJamCUfLB1gjE9YP25kfXtxNg8WTMiFuoD5gtUa3Evk3gViFADogBeDhWBjHNDJoQ44sWzQzaoTH4qcj"
 842ACCOUNT_2_ADDRESS = sp.address("tz1NDUP7uyQNFsSzamkPMfbxirMrg3D6TR2w")
 843ACCOUNT_2_PUBLIC_KEY = sp.key("edpktwD9RYpBiqhFCsaN3t7BbF7uT3zqRfkSWCwuDdfMUDMwDnk9Tz")
 844ACCOUNT_2_SECRET = "edskS4dHu2VNf8eyLhqoMRUtzWdcTMd4y8qJVe19ea5P2N2D1M4Puizfbh9zoRpHAASPYtMcSRbrVFC127EY11nDJPEH62cYKR"
 845
 846################
 847# + Test Helpers
 848################
 849
 850
 851def compute_latest_data(sc, aggregator):
 852    data = aggregator.data.rounds[aggregator.data.latestRoundId]
 853    return sc.compute(data)
 854
 855
 856def add_oracle(oracle, startingRound, endingRound, lastStartedRound):
 857    return sp.pair(
 858        oracle,
 859        sp.record(
 860            startingRound=startingRound,
 861            endingRound=sp.Some(endingRound),
 862            lastStartedRound=lastStartedRound,
 863        ),
 864    )
 865
 866
 867################
 868# - Test Helpers
 869################
 870
 871###################################
 872# Multisign Administrator Helpers #
 873###################################
 874
 875
 876class MSAggregatorHelper:
 877    def variant(content):
 878        return sp.variant.targetAdmin(content)
 879
 880    def changeAdmin(admin):
 881        return sp.variant.changeAdmin(admin)
 882
 883    def changeActive(active):
 884        return sp.variant.changeActive(active)
 885
 886    def changeOracles(removed=[], added=[]):
 887        return sp.variant(
 888            "changeOracles", sp.record(removed=sp.list(removed), added=sp.list(added))
 889        )
 890
 891    def oracle(address, admin, startingRound, endingRound=None):
 892        oracle_pair = sp.pair(
 893            address,
 894            sp.record(
 895                startingRound=startingRound, endingRound=endingRound, adminAddress=admin
 896            ),
 897        )
 898        sp.set_type_expr(
 899            oracle_pair,
 900            sp.pair[
 901                sp.address,
 902                sp.record(
 903                    startingRound=sp.nat,
 904                    endingRound=sp.option[sp.nat],
 905                    adminAddress=sp.address,
 906                ),
 907            ],
 908        )
 909        return oracle_pair
 910
 911    def updateFutureRounds(
 912        minSubmissions, maxSubmissions, restartDelay, timeout, oraclePayment
 913    ):
 914        return sp.variant(
 915            "updateFutureRounds",
 916            sp.record(
 917                minSubmissions=minSubmissions,
 918                maxSubmissions=maxSubmissions,
 919                restartDelay=restartDelay,
 920                timeout=timeout,
 921                oraclePayment=oraclePayment,
 922            ),
 923        )
 924
 925
 926MSAH = MSAggregatorHelper
 927
 928
 929class MSProxyHelper:
 930    def variant(content):
 931        return sp.variant.targetAdmin(content)
 932
 933    def changeAggregator(aggregator):
 934        return sp.variant.changeAggregator(aggregator)
 935
 936    def changeAdmin(admin):
 937        return sp.variant.changeAdmin(admin)
 938
 939    def changeActive(active):
 940        return sp.variant.changeActive(active)
 941
 942
 943#########
 944# + Tests
 945#########
 946@sp.add_test()
 947def test():
 948    sc = sp.test_scenario("ChainlinkPriceFeed", [FA2.t, FA2.main, MSA.m2, m])
 949    sc.h1("ChainLink PriceFeed")
 950
 951    FALSE_ADMIN_ADDRESS = sp.test_account("FALSE ADMIN").address
 952
 953    sc.show(
 954        [
 955            ADMIN_ADDRESS,
 956            AGGREGATOR_ADDRESS,
 957            PROXY_ADDRESS,
 958            TOKEN_ADDRESS,
 959            ORACLE_1_ADDRESS,
 960            ORACLE_2_ADDRESS,
 961            ORACLE_3_ADDRESS,
 962            ORACLE_4_ADDRESS,
 963            ORACLE_5_ADDRESS,
 964            ORACLE_6_ADDRESS,
 965        ]
 966    )
 967
 968    sc.h2("Link Token")
 969    linkToken = m.LinkToken(
 970        administrator=ADMIN_ADDRESS,
 971        metadata=sp.big_map(),
 972        ledger={ADMIN_ADDRESS: 50000000},
 973        token_metadata=FA2.make_metadata(
 974            name="wrapped LINK", decimals=18, symbol="wLINK"
 975        ),
 976    )
 977    sc += linkToken
 978
 979    sc.h2("Aggregator")
 980    restartDelay = sp.nat(2)
 981    now = sp.timestamp(500)
 982    oracles = {
 983        ORACLE_1_ADDRESS: sp.record(
 984            startingRound=0,
 985            endingRound=m.MAX_ROUND,
 986            lastStartedRound=0,
 987            withdrawable=0,
 988            adminAddress=ADMIN_ADDRESS,
 989        ),
 990        ORACLE_2_ADDRESS: sp.record(
 991            startingRound=0,
 992            endingRound=m.MAX_ROUND,
 993            lastStartedRound=0,
 994            withdrawable=0,
 995            adminAddress=ADMIN_ADDRESS,
 996        ),
 997        ORACLE_3_ADDRESS: sp.record(
 998            startingRound=0,
 999            endingRound=m.MAX_ROUND,
1000            lastStartedRound=0,
1001            withdrawable=0,
1002            adminAddress=ADMIN_ADDRESS,
1003        ),
1004        ORACLE_4_ADDRESS: sp.record(
1005            startingRound=0,
1006            endingRound=m.MAX_ROUND,
1007            lastStartedRound=0,
1008            withdrawable=0,
1009            adminAddress=ADMIN_ADDRESS,
1010        ),
1011    }
1012    aggregator = m.PriceAggregator(
1013        initialRoundDetails=sp.record(
1014            submissions={},
1015            minSubmissions=0,
1016            maxSubmissions=0,
1017            timeout=0,
1018            activeOracles=sp.set(),
1019        ),
1020        tokenAddress=linkToken.address,
1021        active=True,
1022        decimals=m.DECIMALS,
1023        admin=ADMIN_ADDRESS,
1024        oracles=oracles,
1025        timeout=m.TIMEOUT,
1026        oraclePayment=m.ORACLE_PAYMENT,
1027        minSubmissions=3,
1028        maxSubmissions=6,
1029        restartDelay=restartDelay,
1030        metadata=sp.big_map(),
1031    )
1032    sc += aggregator
1033
1034    # Update aggregator available funds
1035    linkToken.transfer(
1036        [
1037            sp.record(
1038                from_=ADMIN_ADDRESS,
1039                txs=[
1040                    sp.record(
1041                        to_=aggregator.address,
1042                        amount=50000,
1043                        token_id=m.LINK_TOKEN_ID,
1044                    ),
1045                ],
1046            )
1047        ],
1048        _sender=ADMIN_ADDRESS,
1049    )
1050    aggregator.forceBalanceUpdate()
1051    sc.show(aggregator.data.recordedFunds)
1052
1053    sc.h2("Proxy")
1054    latestRoundDataView = sp.contract(
1055        sp.address, AGGREGATOR_ADDRESS, entrypoint="latestRoundData"
1056    ).unwrap_some(error="Invalid Interface")
1057    proxy = m.Proxy(
1058        active=True,
1059        admin=ADMIN_ADDRESS,
1060        aggregator=sp.Some(sp.to_address(latestRoundDataView)),
1061    )
1062    sc += proxy
1063
1064    sc.h2("Viewer")
1065    viewer = m.Viewer(ADMIN_ADDRESS, PROXY_ADDRESS)
1066    sc += viewer
1067
1068    ###################################
1069    # A scenario with multiple rounds #
1070    ###################################
1071
1072    # Round 1
1073    sc.h2("A complete round")
1074    roundId = 1
1075    price = 500
1076    sc.h3(f"Oracle 1 submits value in round {roundId}")
1077    now = now.add_minutes(1)
1078    aggregator.submit((roundId, price + 0), _sender=ORACLE_1_ADDRESS, _now=now)
1079    sc.h3(f"Oracle 2 submits value in round {roundId}")
1080    now = now.add_minutes(1)
1081    aggregator.submit((roundId, price + 5), _sender=ORACLE_2_ADDRESS, _now=now)
1082    sc.h3(f"Oracle 3 submits value in round {roundId}")
1083    now = now.add_minutes(1)
1084    aggregator.submit((roundId, price + 2), _sender=ORACLE_3_ADDRESS, _now=now)
1085    sc.h3(f"Quorum is reached in round {roundId}")
1086    # Verify answer value
1087    sc.verify(compute_latest_data(sc, aggregator).answer == price + 2)
1088    sc.verify(compute_latest_data(sc, aggregator).roundId == roundId)
1089    sc.verify(compute_latest_data(sc, aggregator).answeredInRound == roundId)
1090    sc.show(aggregator.data.rounds[aggregator.data.latestRoundId])
1091    sc.h3(f"Oracle 4 submits value in round {roundId}")
1092    now = now.add_minutes(1)
1093    aggregator.submit((roundId, price - 5), _sender=ORACLE_4_ADDRESS, _now=now)
1094    sc.h3(f"Answer is updated in round {roundId}")
1095    # Verify answer value
1096    sc.verify(compute_latest_data(sc, aggregator).answer == (price + price + 2) // 2)
1097    sc.verify(compute_latest_data(sc, aggregator).roundId == roundId)
1098    sc.verify(compute_latest_data(sc, aggregator).answeredInRound == roundId)
1099    sc.show(aggregator.data.rounds[aggregator.data.latestRoundId])
1100    sc.h3(f"Oracle 5 is not assigned")
1101    now = now.add_minutes(1)
1102    aggregator.submit((roundId, price - 5), _sender=ORACLE_5_ADDRESS, _valid=False)
1103
1104    # Round 2
1105    sc.h2("A timed out round")
1106    roundId += 1
1107    price = 502
1108    sc.h3(f"Oracle 1 fails to start a new round {roundId}")
1109    now = now.add_minutes(1)
1110    aggregator.submit(
1111        (roundId, price + 0), _sender=ORACLE_1_ADDRESS, _now=now, _valid=False
1112    )
1113    sc.h3(f"Oracle 2 starts a new round {roundId + 1}")
1114    now = now.add_minutes(1)
1115    aggregator.submit((roundId, price + 1), _sender=ORACLE_2_ADDRESS, _now=now)
1116
1117    # Round 3
1118    roundId += 1
1119    now = now.add_minutes(11)
1120    sc.h3(f"Round {roundId} timed out")
1121    sc.h2("A new round")
1122    sc.h3(f"Oracle 3 starts a new round in round {roundId}")
1123    aggregator.submit((roundId, price + 1), _sender=ORACLE_3_ADDRESS, _now=now)
1124    sc.h3(f"Oracle 1 fails to submit value in previous round {roundId - 1}, timed out")
1125    now = now.add_minutes(1)
1126    aggregator.submit((roundId - 1, price - 1), _sender=ORACLE_1_ADDRESS, _now=now)
1127    sc.h3(f"Quorum is reached in previous round {roundId - 1}")
1128    sc.verify(compute_latest_data(sc, aggregator).answer == price - 1)
1129    sc.verify(compute_latest_data(sc, aggregator).roundId == roundId - 2)
1130    sc.verify(compute_latest_data(sc, aggregator).answeredInRound == roundId - 2)
1131
1132    sc.show(aggregator.data.rounds[aggregator.data.latestRoundId])
1133    sc.h3(f"Oracle 1 submits value in current round {roundId}")
1134    now = now.add_minutes(1)
1135    aggregator.submit((roundId, price + 2), _sender=ORACLE_1_ADDRESS, _now=now)
1136    sc.h3(f"Oracle 4 submits value in previous round {roundId - 1}")
1137    now = now.add_minutes(1)
1138    aggregator.submit((roundId, price + 3), _sender=ORACLE_4_ADDRESS, _now=now)
1139    sc.h3(f"Quorum was reached in round {roundId - 1}")
1140    sc.verify(compute_latest_data(sc, aggregator).answer == price + 2)
1141    sc.verify(compute_latest_data(sc, aggregator).roundId == roundId)
1142    sc.verify(compute_latest_data(sc, aggregator).answeredInRound == roundId)
1143    sc.show(aggregator.data.rounds[aggregator.data.latestRoundId])
1144
1145    # Round 4
1146    sc.h2("A new round")
1147    roundId += 1
1148    price = 515
1149    sc.h3(f"Oracle 4 starts a new round {roundId}")
1150    now = now.add_minutes(1)
1151    aggregator.submit((roundId, price), _sender=ORACLE_4_ADDRESS, _now=now)
1152    sc.h3(f"Oracle 3 fails to start a new round because {roundId} isn't over")
1153    now = now.add_minutes(1)
1154    aggregator.submit(
1155        (roundId + 1, price), _sender=ORACLE_3_ADDRESS, _now=now, _valid=False
1156    )
1157    sc.h3(f"Oracle 1 submits value in current round {roundId}")
1158    now = now.add_minutes(1)
1159    aggregator.submit((roundId, price + 2), _sender=ORACLE_1_ADDRESS, _now=now)
1160    sc.show(aggregator.data.rounds[sp.as_nat(aggregator.data.latestRoundId - 1)])
1161    sc.h3(f"Oracle 2 submits value in previous round {roundId - 1}")
1162    now = now.add_minutes(1)
1163    aggregator.submit((roundId - 1, price - 10), _sender=ORACLE_2_ADDRESS, _now=now)
1164    sc.h3(f"Answer is updated in round {roundId - 1}")
1165    sc.verify(compute_latest_data(sc, aggregator).answer == 504)
1166    sc.verify(compute_latest_data(sc, aggregator).roundId == roundId - 1)
1167    sc.verify(compute_latest_data(sc, aggregator).answeredInRound == roundId)
1168    sc.show(aggregator.data.rounds[aggregator.data.latestRoundId])
1169
1170    ##########
1171    # Withdraw oracle payment
1172    aggregator.withdrawPayment(
1173        oracleAddress=ORACLE_2_ADDRESS,
1174        recipientAddress=ORACLE_2_ADDRESS,
1175        amount=2,
1176        _sender=ADMIN_ADDRESS,
1177    )
1178
1179    ##########
1180    # A viewer
1181    sc.h2("Viewer get latestRoundData")
1182    viewer.getLatestRoundData()
1183
1184    #############################
1185    # Aggregator's administration
1186
1187    sc.h2("Administration")
1188    sc.h3("False Admin tries to administrate")
1189    updateFutureRounds = MSAH.updateFutureRounds(
1190        restartDelay=2,
1191        minSubmissions=4,
1192        maxSubmissions=6,
1193        timeout=m.TIMEOUT,
1194        oraclePayment=m.ORACLE_PAYMENT,
1195    )
1196    aggregator.administrate(
1197        [updateFutureRounds], _sender=FALSE_ADMIN_ADDRESS, _valid=False
1198    )
1199
1200    sc.h3("Admin sets Admin 2 as Admin")
1201    changeAdmin = MSAH.changeAdmin(FALSE_ADMIN_ADDRESS)
1202    aggregator.administrate([changeAdmin], _sender=ADMIN_ADDRESS)
1203
1204    sc.h3("Admin 2 administrates futureRounds and removes Oracle2")
1205    updateFutureRounds = MSAH.updateFutureRounds(
1206        restartDelay=0,
1207        minSubmissions=2,
1208        maxSubmissions=4,
1209        timeout=m.TIMEOUT,
1210        oraclePayment=m.ORACLE_PAYMENT,
1211    )
1212    updateOracles = MSAH.changeOracles(removed=[ORACLE_2_ADDRESS])
1213    aggregator.administrate(
1214        [updateFutureRounds, updateOracles], _sender=FALSE_ADMIN_ADDRESS
1215    )
1216
1217    ######################################
1218    # Administration via multisign admin #
1219    ######################################
1220    sc.h2("Multisign administration contract")
1221    now = now.add_minutes(1)
1222    multisignAdmin = MSA.m2.MultisignAdmin(
1223        quorum=1,
1224        timeout=5,
1225        addrVoterId=sp.big_map({ACCOUNT_1_ADDRESS: 0}),
1226        keyVoterId=sp.big_map({ACCOUNT_1_PUBLIC_KEY: 0}),
1227        voters={
1228            0: sp.record(
1229                addr=ACCOUNT_1_ADDRESS, publicKey=ACCOUNT_1_PUBLIC_KEY, lastProposalId=0
1230            )
1231        },
1232        lastVoterId=0,
1233        metadata=sp.scenario_utils.metadata_of_url(
1234            "ipfs://QmWGLWx4pGZBrVF9Bz12pAT3A5Dunw3o9NMAKVvWK51Cfy"
1235        ),
1236    )
1237    sc += multisignAdmin
1238
1239    sc.h2("Signers")
1240    sc.show([ACCOUNT_1_ADDRESS, ACCOUNT_2_ADDRESS])
1241
1242    class signer1:
1243        address = ACCOUNT_1_ADDRESS
1244
1245    class signer2:
1246        address = ACCOUNT_2_ADDRESS
1247
1248    sc.h2("ACCOUNT_1 adds ACCOUNT_2, change quorum and set aggregator as target")
1249    changeVoters = MSA.SelfHelper.changeVoters(
1250        added=[(ACCOUNT_2_ADDRESS, ACCOUNT_2_PUBLIC_KEY)]
1251    )
1252    changeQuorum = MSA.SelfHelper.changeQuorum(2)
1253    targetAddress = sp.contract(
1254        sp.list[m.t_aggregatorAdminAction],
1255        aggregator.address,
1256        entrypoint="administrate",
1257    ).unwrap_some()
1258    changeTarget = MSA.SelfHelper.changeTarget(sp.to_address(targetAddress))
1259    multisignAdmin.newProposal(
1260        [changeVoters, changeQuorum, changeTarget], _sender=ACCOUNT_1_ADDRESS, _now=now
1261    )
1262
1263    sc.h2("Aggregator admin sets multisign as administrator")
1264    changeAdmin = MSAH.changeAdmin(multisignAdmin.address)
1265    aggregator.administrate([changeAdmin], _sender=FALSE_ADMIN_ADDRESS)
1266
1267    sc.h2("Adding back Oracle2 via multisign")
1268    sc.h3("New Proposal by ACCOUNT_1")
1269    now = now.add_minutes(1)
1270    oracle2 = MSAH.oracle(ORACLE_2_ADDRESS, ADMIN_ADDRESS, roundId + 1)
1271    changeOracles = MSAH.changeOracles(added=[oracle2])
1272    multisignAdmin.newProposal(
1273        [MSAH.variant([changeOracles])], _sender=ACCOUNT_1_ADDRESS, _now=now
1274    )
1275    now = now.add_minutes(1)
1276    sc.h3("ACCOUNT_2 votes the proposal")
1277    sc.verify(~aggregator.data.oracles.contains(ORACLE_2_ADDRESS))
1278    multisignAdmin.vote(
1279        [MSA.vote(multisignAdmin, signer1, yay=True)], _sender=ACCOUNT_2_ADDRESS
1280    )
1281    sc.verify(aggregator.data.oracles.contains(ORACLE_2_ADDRESS))
1282
1283    sc.h2("An administration proposal that fails on Aggregator")
1284    sc.h3("New Proposal by ACCOUNT_1")
1285    updateFutureRounds = MSAH.updateFutureRounds(10, 10, 10, 10, 10)
1286    multisignAdmin.newProposal(
1287        [MSAH.variant([updateFutureRounds])], _sender=ACCOUNT_1_ADDRESS, _now=now
1288    )
1289    now = now.add_minutes(1)
1290    sc.h3("ACCOUNT_2 votes the proposal")
1291    multisignAdmin.vote(
1292        [MSA.vote(multisignAdmin, signer1, yay=True)],
1293        _sender=ACCOUNT_2_ADDRESS,
1294        _valid=False,
1295    )
main = None
GENERATE_DEPLOYMENT_CONTRACTS = False
ADMIN_ADDRESS = (("templates/price_feed.py" 826) literal (address "tz1evBmfWZoPDN38avoRGbjJaLBJUP8AZz6a"))
AGGREGATOR_ADDRESS = (("templates/price_feed.py" 827) literal (address "KT1CfuSjCcunNQ5qCCro2Kc74uivnor9d8ba"))
PROXY_ADDRESS = (("templates/price_feed.py" 828) literal (address "KT1PG6uK91ymZYVtjnRXv2mEdFYSH6P6uJhC"))
TOKEN_ADDRESS = (("templates/price_feed.py" 829) literal (address "KT1LcrXERzpDeUXWxLEWnLipHrhWEhzSRTt7"))
ORACLE_1_ADDRESS = (("templates/price_feed.py" 831) literal (address "KT1LhTzYhdhxTqKu7ByJz8KaShF6qPTdx5os"))
ORACLE_2_ADDRESS = (("templates/price_feed.py" 832) literal (address "KT1P7oeoKWHx5SXt73qpEanzkr8yeEKABqko"))
ORACLE_3_ADDRESS = (("templates/price_feed.py" 833) literal (address "KT1SCkxmTqTkmc7zoAP5uMYT9rp9iqVVRgdt"))
ORACLE_4_ADDRESS = (("templates/price_feed.py" 834) literal (address "KT1LLTzYhdhxTqKu7ByJz8KaShF6qPTdx5os"))
ORACLE_5_ADDRESS = (("templates/price_feed.py" 835) literal (address "KT1P6oeoKWHx5SXt73qpEanzkr8yeEKABqko"))
ORACLE_6_ADDRESS = (("templates/price_feed.py" 836) literal (address "KT1SskxmTqTkmc7zoAP5uMYT9rp9iqVVRgdt"))
ACCOUNT_1_ADDRESS = (("templates/price_feed.py" 839) literal (address "tz1evBmfWZoPDN38avoRGbjJaLBJUP8AZz6a"))
ACCOUNT_1_PUBLIC_KEY = (("templates/price_feed.py" 840) literal (key "edpkuZ7ERiU5B8knLqQsVMH86j9RLMUyHyL665oCXDkPQxF7HGqSeJ"))
ACCOUNT_1_SECRET = 'edskRowES25ZTKFDV5GJamCUfLB1gjE9YP25kfXtxNg8WTMiFuoD5gtUa3Evk3gViFADogBeDhWBjHNDJoQ44sWzQzaoTH4qcj'
ACCOUNT_2_ADDRESS = (("templates/price_feed.py" 842) literal (address "tz1NDUP7uyQNFsSzamkPMfbxirMrg3D6TR2w"))
ACCOUNT_2_PUBLIC_KEY = (("templates/price_feed.py" 843) literal (key "edpktwD9RYpBiqhFCsaN3t7BbF7uT3zqRfkSWCwuDdfMUDMwDnk9Tz"))
ACCOUNT_2_SECRET = 'edskS4dHu2VNf8eyLhqoMRUtzWdcTMd4y8qJVe19ea5P2N2D1M4Puizfbh9zoRpHAASPYtMcSRbrVFC127EY11nDJPEH62cYKR'
def compute_latest_data(sc, aggregator):
852def compute_latest_data(sc, aggregator):
853    data = aggregator.data.rounds[aggregator.data.latestRoundId]
854    return sc.compute(data)
def add_oracle(oracle, startingRound, endingRound, lastStartedRound):
857def add_oracle(oracle, startingRound, endingRound, lastStartedRound):
858    return sp.pair(
859        oracle,
860        sp.record(
861            startingRound=startingRound,
862            endingRound=sp.Some(endingRound),
863            lastStartedRound=lastStartedRound,
864        ),
865    )
class MSAggregatorHelper:
877class MSAggregatorHelper:
878    def variant(content):
879        return sp.variant.targetAdmin(content)
880
881    def changeAdmin(admin):
882        return sp.variant.changeAdmin(admin)
883
884    def changeActive(active):
885        return sp.variant.changeActive(active)
886
887    def changeOracles(removed=[], added=[]):
888        return sp.variant(
889            "changeOracles", sp.record(removed=sp.list(removed), added=sp.list(added))
890        )
891
892    def oracle(address, admin, startingRound, endingRound=None):
893        oracle_pair = sp.pair(
894            address,
895            sp.record(
896                startingRound=startingRound, endingRound=endingRound, adminAddress=admin
897            ),
898        )
899        sp.set_type_expr(
900            oracle_pair,
901            sp.pair[
902                sp.address,
903                sp.record(
904                    startingRound=sp.nat,
905                    endingRound=sp.option[sp.nat],
906                    adminAddress=sp.address,
907                ),
908            ],
909        )
910        return oracle_pair
911
912    def updateFutureRounds(
913        minSubmissions, maxSubmissions, restartDelay, timeout, oraclePayment
914    ):
915        return sp.variant(
916            "updateFutureRounds",
917            sp.record(
918                minSubmissions=minSubmissions,
919                maxSubmissions=maxSubmissions,
920                restartDelay=restartDelay,
921                timeout=timeout,
922                oraclePayment=oraclePayment,
923            ),
924        )
def variant(content):
878    def variant(content):
879        return sp.variant.targetAdmin(content)
def changeAdmin(admin):
881    def changeAdmin(admin):
882        return sp.variant.changeAdmin(admin)
def changeActive(active):
884    def changeActive(active):
885        return sp.variant.changeActive(active)
def changeOracles(removed=[], added=[]):
887    def changeOracles(removed=[], added=[]):
888        return sp.variant(
889            "changeOracles", sp.record(removed=sp.list(removed), added=sp.list(added))
890        )
def oracle(address, admin, startingRound, endingRound=None):
892    def oracle(address, admin, startingRound, endingRound=None):
893        oracle_pair = sp.pair(
894            address,
895            sp.record(
896                startingRound=startingRound, endingRound=endingRound, adminAddress=admin
897            ),
898        )
899        sp.set_type_expr(
900            oracle_pair,
901            sp.pair[
902                sp.address,
903                sp.record(
904                    startingRound=sp.nat,
905                    endingRound=sp.option[sp.nat],
906                    adminAddress=sp.address,
907                ),
908            ],
909        )
910        return oracle_pair
def updateFutureRounds(minSubmissions, maxSubmissions, restartDelay, timeout, oraclePayment):
912    def updateFutureRounds(
913        minSubmissions, maxSubmissions, restartDelay, timeout, oraclePayment
914    ):
915        return sp.variant(
916            "updateFutureRounds",
917            sp.record(
918                minSubmissions=minSubmissions,
919                maxSubmissions=maxSubmissions,
920                restartDelay=restartDelay,
921                timeout=timeout,
922                oraclePayment=oraclePayment,
923            ),
924        )
class MSProxyHelper:
930class MSProxyHelper:
931    def variant(content):
932        return sp.variant.targetAdmin(content)
933
934    def changeAggregator(aggregator):
935        return sp.variant.changeAggregator(aggregator)
936
937    def changeAdmin(admin):
938        return sp.variant.changeAdmin(admin)
939
940    def changeActive(active):
941        return sp.variant.changeActive(active)
def variant(content):
931    def variant(content):
932        return sp.variant.targetAdmin(content)
def changeAggregator(aggregator):
934    def changeAggregator(aggregator):
935        return sp.variant.changeAggregator(aggregator)
def changeAdmin(admin):
937    def changeAdmin(admin):
938        return sp.variant.changeAdmin(admin)
def changeActive(active):
940    def changeActive(active):
941        return sp.variant.changeActive(active)