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):
def
add_oracle(oracle, startingRound, endingRound, lastStartedRound):
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
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 )
MSAH =
<class 'templates.price_feed.MSAggregatorHelper'>
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)