├── Bridge2.sol ├── Signature.sol └── tests ├── bridge_watcher2.rs └── example.rs /Bridge2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | /* 4 | This bridge contract runs on Arbitrum, operating alongside the Hyperliquid L1. 5 | The only asset for now is USDC, though the logic extends to any other ERC20 token on Arbitrum. 6 | The L1 runs tendermint consensus, with validator set updates happening at the end of each epoch. 7 | Epoch duration TBD, but likely somewhere between 1 day and 1 week. 8 | "Bridge2" is to distinguish from the legacy Bridge contract. 9 | 10 | Validators: 11 | Each validator has a hot (in memory) and cold wallet. 12 | Automated withdrawals and validator set updates are approved by 2/3 of the validator power, 13 | signed by hot wallets. 14 | For additional security, withdrawals and validator set updates are pending for a dispute period. 15 | During this period, any locker may lock the bridge (preventing further withdrawals or updates). 16 | To unlock the bridge, a quorum of cold wallet signatures is required. 17 | 18 | Validator set updates: 19 | The active validators sign a hash of the new validator set and powers on the L1. 20 | This contract checks those signatures, and updates the hash of the active validator set. 21 | The active validators' L1 stake is still locked for at least one more epoch (unbonding period), 22 | and the new validators will slash the old ones' stake if they do not properly generate the validator set update signatures. 23 | The validator set change is pending for a period of time for the lockers to dispute the change. 24 | 25 | Withdrawals: 26 | The validators sign withdrawals on the L1, which are batched and sent to batchedRequestWithdrawals() 27 | This contract checks the signatures, and then creates a pending withdrawal which can be disputed for a period of time. 28 | After the dispute period has elapsed (measured in both time and blocks), a second transaction can be sent to finalize the withdrawal and release the USDC. 29 | 30 | Deposits: 31 | The validators on the L1 listen for and sign DepositEvent events emitted by this contract, 32 | crediting the L1 with the equivalent USDC. No additional work needs to be done on this contract. 33 | 34 | Signatures: 35 | For withdrawals and validator set updates, the signatures are sent to the bridge contract 36 | in the same order as the active validator set, i.e. signing validators should be a subsequence 37 | of active validators. 38 | 39 | Lockers: 40 | These addresses are approved by the validators to lock the contract if submitted signatures do not match 41 | the locker's view of the L1. Once locked, only a quorum of cold wallet validator signatures can unlock the bridge. 42 | This dispute period is used for both withdrawals and validator set updates. 43 | L1 operation will automatically register all validator hot addresses as lockers. 44 | Adding a locker requires hot wallet quorum, and removing requires cold wallet quorum. 45 | 46 | Finalizers: 47 | These addresses are approved by the validators to finalize withdrawals and validator set updates. 48 | While not strictly necessary due to the locking mechanism, this adds an additional layer of security without sacrificing functionality. 49 | Even if locking transactions are censored (which should be economically infeasible), this still requires attackers to control a finalizer private key. 50 | L1 operation will eventually register all validator hot addresses as finalizers, 51 | though there may be an intermediate phase where finalizers are a subset of trusted validators. 52 | Adding a finalizer requires hot wallet quorum, and removing requires cold wallet quorum. 53 | 54 | Unlocking: 55 | When the bridge is unlocked, a new validator set is atomically set and finalized. 56 | This is safe because the unlocking message is signed by a quorum of validator cold wallets. 57 | 58 | The L1 will ensure the following, though neither is required by the smart contract: 59 | 1. The order of active validators are ordered in decreasing order of power. 60 | 2. The validators are unique. 61 | 62 | On epoch changes, the L1 will ensure that new signatures are generated for unclaimed withdrawals 63 | for any validators that have changed. 64 | 65 | This bridge contract assumes there will be 20-30 validators on the L1, so signature sets fit in a single tx. 66 | */ 67 | 68 | pragma solidity ^0.8.9; 69 | 70 | import "@openzeppelin/contracts/security/Pausable.sol"; 71 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 72 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 73 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 74 | import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol"; 75 | import "@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol"; 76 | import "./Signature.sol"; 77 | 78 | struct ValidatorSet { 79 | uint64 epoch; 80 | address[] validators; 81 | uint64[] powers; 82 | } 83 | 84 | struct ValidatorSetUpdateRequest { 85 | uint64 epoch; 86 | address[] hotAddresses; 87 | address[] coldAddresses; 88 | uint64[] powers; 89 | } 90 | 91 | struct PendingValidatorSetUpdate { 92 | uint64 epoch; 93 | uint64 totalValidatorPower; 94 | uint64 updateTime; 95 | uint64 updateBlockNumber; 96 | uint64 nValidators; 97 | bytes32 hotValidatorSetHash; 98 | bytes32 coldValidatorSetHash; 99 | } 100 | 101 | struct Withdrawal { 102 | address user; 103 | address destination; 104 | uint64 usd; 105 | uint64 nonce; 106 | uint64 requestedTime; 107 | uint64 requestedBlockNumber; 108 | bytes32 message; 109 | } 110 | 111 | struct WithdrawalRequest { 112 | address user; 113 | address destination; 114 | uint64 usd; 115 | uint64 nonce; 116 | Signature[] signatures; 117 | } 118 | 119 | struct DepositWithPermit { 120 | address user; 121 | uint64 usd; 122 | uint64 deadline; 123 | Signature signature; 124 | } 125 | 126 | contract Bridge2 is Pausable, ReentrancyGuard { 127 | using SafeERC20 for ERC20Permit; 128 | ERC20Permit public usdcToken; 129 | 130 | bytes32 public hotValidatorSetHash; 131 | bytes32 public coldValidatorSetHash; 132 | PendingValidatorSetUpdate public pendingValidatorSetUpdate; 133 | 134 | mapping(bytes32 => bool) public usedMessages; 135 | mapping(address => bool) public lockers; 136 | address[] private lockersVotingLock; 137 | uint64 public lockerThreshold; 138 | 139 | mapping(address => bool) public finalizers; 140 | uint64 public epoch; 141 | uint64 public totalValidatorPower; 142 | uint64 public disputePeriodSeconds; 143 | // Need higher resolution than seconds for Arbitrum. 144 | uint64 public blockDurationMillis; 145 | 146 | // Expose this for convenience because we only store the hash. 147 | // The uniqueness of the validators is enforced on the L1 side. 148 | // However, no functionality breaks even if addresses are repeated. 149 | uint64 public nValidators; 150 | 151 | mapping(bytes32 => Withdrawal) public requestedWithdrawals; 152 | mapping(bytes32 => bool) public finalizedWithdrawals; 153 | mapping(bytes32 => bool) public withdrawalsInvalidated; 154 | 155 | bytes32 immutable domainSeparator; 156 | 157 | event Deposit(address indexed user, uint64 usd); 158 | 159 | event RequestedWithdrawal( 160 | address indexed user, 161 | address destination, 162 | uint64 usd, 163 | uint64 nonce, 164 | bytes32 message, 165 | uint64 requestedTime 166 | ); 167 | 168 | event FinalizedWithdrawal( 169 | address indexed user, 170 | address destination, 171 | uint64 usd, 172 | uint64 nonce, 173 | bytes32 message 174 | ); 175 | 176 | event RequestedValidatorSetUpdate( 177 | uint64 epoch, 178 | bytes32 hotValidatorSetHash, 179 | bytes32 coldValidatorSetHash, 180 | uint64 updateTime 181 | ); 182 | 183 | event FinalizedValidatorSetUpdate( 184 | uint64 epoch, 185 | bytes32 hotValidatorSetHash, 186 | bytes32 coldValidatorSetHash 187 | ); 188 | 189 | event FailedWithdrawal(bytes32 message, uint32 errorCode); 190 | event ModifiedLocker(address indexed locker, bool isLocker); 191 | event FailedPermitDeposit(address user, uint64 usd, uint32 errorCode); 192 | event ModifiedFinalizer(address indexed finalizer, bool isFinalizer); 193 | event ChangedDisputePeriodSeconds(uint64 newDisputePeriodSeconds); 194 | event ChangedBlockDurationMillis(uint64 newBlockDurationMillis); 195 | event ChangedLockerThreshold(uint64 newLockerThreshold); 196 | event InvalidatedWithdrawal(Withdrawal withdrawal); 197 | 198 | // We could have the deployer initialize separately so that all function args in this file can be calldata. 199 | // However, calldata does not seem cheaper than memory on Arbitrum, so not a big deal for now. 200 | constructor( 201 | address[] memory hotAddresses, 202 | address[] memory coldAddresses, 203 | uint64[] memory powers, 204 | address usdcAddress, 205 | uint64 _disputePeriodSeconds, 206 | uint64 _blockDurationMillis, 207 | uint64 _lockerThreshold 208 | ) { 209 | domainSeparator = makeDomainSeparator(); 210 | totalValidatorPower = checkNewValidatorPowers(powers); 211 | 212 | require( 213 | hotAddresses.length == coldAddresses.length, 214 | "Hot and cold validator sets length mismatch" 215 | ); 216 | nValidators = uint64(hotAddresses.length); 217 | 218 | ValidatorSet memory hotValidatorSet; 219 | hotValidatorSet = ValidatorSet({ epoch: 0, validators: hotAddresses, powers: powers }); 220 | bytes32 newHotValidatorSetHash = makeValidatorSetHash(hotValidatorSet); 221 | hotValidatorSetHash = newHotValidatorSetHash; 222 | 223 | ValidatorSet memory coldValidatorSet; 224 | coldValidatorSet = ValidatorSet({ epoch: 0, validators: coldAddresses, powers: powers }); 225 | bytes32 newColdValidatorSetHash = makeValidatorSetHash(coldValidatorSet); 226 | coldValidatorSetHash = newColdValidatorSetHash; 227 | 228 | usdcToken = ERC20Permit(usdcAddress); 229 | disputePeriodSeconds = _disputePeriodSeconds; 230 | blockDurationMillis = _blockDurationMillis; 231 | lockerThreshold = _lockerThreshold; 232 | addLockersAndFinalizers(hotAddresses); 233 | 234 | emit RequestedValidatorSetUpdate( 235 | 0, 236 | hotValidatorSetHash, 237 | coldValidatorSetHash, 238 | uint64(block.timestamp) 239 | ); 240 | 241 | pendingValidatorSetUpdate = PendingValidatorSetUpdate({ 242 | epoch: 0, 243 | totalValidatorPower: totalValidatorPower, 244 | updateTime: 0, 245 | updateBlockNumber: getCurBlockNumber(), 246 | hotValidatorSetHash: hotValidatorSetHash, 247 | coldValidatorSetHash: coldValidatorSetHash, 248 | nValidators: nValidators 249 | }); 250 | 251 | emit FinalizedValidatorSetUpdate(0, hotValidatorSetHash, coldValidatorSetHash); 252 | } 253 | 254 | function addLockersAndFinalizers(address[] memory addresses) private { 255 | uint64 end = uint64(addresses.length); 256 | for (uint64 idx; idx < end; idx++) { 257 | address _address = addresses[idx]; 258 | lockers[_address] = true; 259 | finalizers[_address] = true; 260 | } 261 | } 262 | 263 | // A utility function to make a checkpoint of the validator set supplied. 264 | // The checkpoint is the hash of all the validators, the powers and the epoch. 265 | function makeValidatorSetHash(ValidatorSet memory validatorSet) private pure returns (bytes32) { 266 | require( 267 | validatorSet.validators.length == validatorSet.powers.length, 268 | "Malformed validator set" 269 | ); 270 | 271 | bytes32 checkpoint = keccak256( 272 | abi.encode(validatorSet.validators, validatorSet.powers, validatorSet.epoch) 273 | ); 274 | return checkpoint; 275 | } 276 | 277 | function requestWithdrawal( 278 | address user, 279 | address destination, 280 | uint64 usd, 281 | uint64 nonce, 282 | ValidatorSet calldata hotValidatorSet, 283 | Signature[] memory signatures 284 | ) internal { 285 | // NOTE: this is a temporary workaround because EIP-191 signatures do not match between rust client and solidity. 286 | // For now we do not care about the overhead with EIP-712 because Arbitrum gas is cheap. 287 | bytes32 data = keccak256(abi.encode("requestWithdrawal", user, destination, usd, nonce)); 288 | bytes32 message = makeMessage(data); 289 | if (!isValidWithdrawal(message)) { 290 | emit FailedWithdrawal(message, 5); 291 | return; 292 | } 293 | Withdrawal memory withdrawal = Withdrawal({ 294 | user: user, 295 | destination: destination, 296 | usd: usd, 297 | nonce: nonce, 298 | requestedTime: uint64(block.timestamp), 299 | requestedBlockNumber: getCurBlockNumber(), 300 | message: message 301 | }); 302 | if (requestedWithdrawals[message].requestedTime != 0) { 303 | emit FailedWithdrawal(message, 0); 304 | return; 305 | } 306 | checkValidatorSignatures(message, hotValidatorSet, signatures, hotValidatorSetHash); 307 | requestedWithdrawals[message] = withdrawal; 308 | emit RequestedWithdrawal( 309 | withdrawal.user, 310 | withdrawal.destination, 311 | withdrawal.usd, 312 | withdrawal.nonce, 313 | withdrawal.message, 314 | withdrawal.requestedTime 315 | ); 316 | } 317 | 318 | // An external function anyone can call to withdraw usdc from the bridge by providing valid signatures 319 | // from the active L1 validators. 320 | function batchedRequestWithdrawals( 321 | WithdrawalRequest[] memory withdrawalRequests, 322 | ValidatorSet calldata hotValidatorSet 323 | ) external nonReentrant whenNotPaused { 324 | uint64 end = uint64(withdrawalRequests.length); 325 | for (uint64 idx; idx < end; idx++) { 326 | WithdrawalRequest memory withdrawalRequest = withdrawalRequests[idx]; 327 | requestWithdrawal( 328 | withdrawalRequest.user, 329 | withdrawalRequest.destination, 330 | withdrawalRequest.usd, 331 | withdrawalRequest.nonce, 332 | hotValidatorSet, 333 | withdrawalRequest.signatures 334 | ); 335 | } 336 | } 337 | 338 | function finalizeWithdrawal(bytes32 message) internal { 339 | if (!isValidWithdrawal(message)) { 340 | emit FailedWithdrawal(message, 5); 341 | return; 342 | } 343 | 344 | if (finalizedWithdrawals[message]) { 345 | emit FailedWithdrawal(message, 1); 346 | return; 347 | } 348 | 349 | Withdrawal memory withdrawal = requestedWithdrawals[message]; 350 | if (withdrawal.requestedTime == 0) { 351 | emit FailedWithdrawal(message, 2); 352 | return; 353 | } 354 | 355 | uint32 errorCode = getDisputePeriodErrorCode( 356 | withdrawal.requestedTime, 357 | withdrawal.requestedBlockNumber 358 | ); 359 | 360 | if (errorCode != 0) { 361 | emit FailedWithdrawal(message, errorCode); 362 | return; 363 | } 364 | 365 | finalizedWithdrawals[message] = true; 366 | usdcToken.safeTransfer(withdrawal.destination, withdrawal.usd); 367 | emit FinalizedWithdrawal( 368 | withdrawal.user, 369 | withdrawal.destination, 370 | withdrawal.usd, 371 | withdrawal.nonce, 372 | withdrawal.message 373 | ); 374 | } 375 | 376 | function batchedFinalizeWithdrawals( 377 | bytes32[] calldata messages 378 | ) external nonReentrant whenNotPaused { 379 | checkFinalizer(msg.sender); 380 | uint64 end = uint64(messages.length); 381 | for (uint64 idx; idx < end; idx++) { 382 | finalizeWithdrawal(messages[idx]); 383 | } 384 | } 385 | 386 | function isValidWithdrawal(bytes32 message) private view returns (bool) { 387 | return !withdrawalsInvalidated[message]; 388 | } 389 | 390 | function getCurBlockNumber() private view returns (uint64) { 391 | if (block.chainid == 1337) { 392 | return uint64(block.number); 393 | } 394 | return uint64(ArbSys(address(100)).arbBlockNumber()); 395 | } 396 | 397 | // Returns 0 if no error 398 | function getDisputePeriodErrorCode( 399 | uint64 time, 400 | uint64 blockNumber 401 | ) private view returns (uint32) { 402 | bool enoughTimePassed = block.timestamp > time + disputePeriodSeconds; 403 | if (!enoughTimePassed) { 404 | return 3; 405 | } 406 | 407 | uint64 curBlockNumber = getCurBlockNumber(); 408 | 409 | bool enoughBlocksPassed = (curBlockNumber - blockNumber) * blockDurationMillis > 410 | 1000 * disputePeriodSeconds; 411 | if (!enoughBlocksPassed) { 412 | return 4; 413 | } 414 | 415 | return 0; 416 | } 417 | 418 | // Utility function that verifies the signatures supplied and checks that the validators have reached quorum. 419 | function checkValidatorSignatures( 420 | bytes32 message, 421 | ValidatorSet memory activeValidatorSet, // Active set of all L1 validators 422 | Signature[] memory signatures, 423 | bytes32 validatorSetHash 424 | ) private view { 425 | require( 426 | makeValidatorSetHash(activeValidatorSet) == validatorSetHash, 427 | "Supplied active validators and powers do not match the active checkpoint" 428 | ); 429 | 430 | uint64 nSignatures = uint64(signatures.length); 431 | require(nSignatures > 0, "Signers empty"); 432 | uint64 cumulativePower; 433 | uint64 signatureIdx; 434 | uint64 end = uint64(activeValidatorSet.validators.length); 435 | 436 | for (uint64 activeValidatorSetIdx; activeValidatorSetIdx < end; activeValidatorSetIdx++) { 437 | address signer = recoverSigner(message, signatures[signatureIdx], domainSeparator); 438 | if (signer == activeValidatorSet.validators[activeValidatorSetIdx]) { 439 | uint64 power = activeValidatorSet.powers[activeValidatorSetIdx]; 440 | cumulativePower += power; 441 | 442 | if (3 * cumulativePower > 2 * totalValidatorPower) { 443 | break; 444 | } 445 | 446 | signatureIdx += 1; 447 | if (signatureIdx >= nSignatures) { 448 | break; 449 | } 450 | } 451 | } 452 | 453 | require( 454 | 3 * cumulativePower > 2 * totalValidatorPower, 455 | "Submitted validator set signatures do not have enough power" 456 | ); 457 | } 458 | 459 | function checkMessageNotUsed(bytes32 message) private { 460 | require(!usedMessages[message], "message already used"); 461 | usedMessages[message] = true; 462 | } 463 | 464 | // This function updates the validator set by checking that the active validators have signed 465 | // off on the new validator set 466 | function updateValidatorSet( 467 | ValidatorSetUpdateRequest memory newValidatorSet, 468 | ValidatorSet memory activeHotValidatorSet, 469 | Signature[] memory signatures 470 | ) external whenNotPaused { 471 | require( 472 | makeValidatorSetHash(activeHotValidatorSet) == hotValidatorSetHash, 473 | "Supplied active validators and powers do not match checkpoint" 474 | ); 475 | 476 | bytes32 data = keccak256( 477 | abi.encode( 478 | "updateValidatorSet", 479 | newValidatorSet.epoch, 480 | newValidatorSet.hotAddresses, 481 | newValidatorSet.coldAddresses, 482 | newValidatorSet.powers 483 | ) 484 | ); 485 | bytes32 message = makeMessage(data); 486 | 487 | updateValidatorSetInner(newValidatorSet, activeHotValidatorSet, signatures, message, false); 488 | } 489 | 490 | function updateValidatorSetInner( 491 | ValidatorSetUpdateRequest memory newValidatorSet, 492 | ValidatorSet memory activeValidatorSet, 493 | Signature[] memory signatures, 494 | bytes32 message, 495 | bool useColdValidatorSet 496 | ) private { 497 | require( 498 | newValidatorSet.hotAddresses.length == newValidatorSet.coldAddresses.length, 499 | "New hot and cold validator sets length mismatch" 500 | ); 501 | 502 | require( 503 | newValidatorSet.hotAddresses.length == newValidatorSet.powers.length, 504 | "New validator set and powers length mismatch" 505 | ); 506 | 507 | require( 508 | newValidatorSet.epoch > activeValidatorSet.epoch, 509 | "New validator set epoch must be greater than the active epoch" 510 | ); 511 | 512 | uint64 newTotalValidatorPower = checkNewValidatorPowers(newValidatorSet.powers); 513 | 514 | bytes32 validatorSetHash; 515 | if (useColdValidatorSet) { 516 | validatorSetHash = coldValidatorSetHash; 517 | } else { 518 | validatorSetHash = hotValidatorSetHash; 519 | } 520 | 521 | checkValidatorSignatures(message, activeValidatorSet, signatures, validatorSetHash); 522 | 523 | ValidatorSet memory newHotValidatorSet; 524 | newHotValidatorSet = ValidatorSet({ 525 | epoch: newValidatorSet.epoch, 526 | validators: newValidatorSet.hotAddresses, 527 | powers: newValidatorSet.powers 528 | }); 529 | bytes32 newHotValidatorSetHash = makeValidatorSetHash(newHotValidatorSet); 530 | 531 | ValidatorSet memory newColdValidatorSet; 532 | newColdValidatorSet = ValidatorSet({ 533 | epoch: newValidatorSet.epoch, 534 | validators: newValidatorSet.coldAddresses, 535 | powers: newValidatorSet.powers 536 | }); 537 | bytes32 newColdValidatorSetHash = makeValidatorSetHash(newColdValidatorSet); 538 | 539 | uint64 updateTime = uint64(block.timestamp); 540 | pendingValidatorSetUpdate = PendingValidatorSetUpdate({ 541 | epoch: newValidatorSet.epoch, 542 | totalValidatorPower: newTotalValidatorPower, 543 | updateTime: updateTime, 544 | updateBlockNumber: getCurBlockNumber(), 545 | hotValidatorSetHash: newHotValidatorSetHash, 546 | coldValidatorSetHash: newColdValidatorSetHash, 547 | nValidators: uint64(newHotValidatorSet.validators.length) 548 | }); 549 | 550 | emit RequestedValidatorSetUpdate( 551 | newValidatorSet.epoch, 552 | newHotValidatorSetHash, 553 | newColdValidatorSetHash, 554 | updateTime 555 | ); 556 | } 557 | 558 | function finalizeValidatorSetUpdate() external nonReentrant whenNotPaused { 559 | checkFinalizer(msg.sender); 560 | 561 | require( 562 | pendingValidatorSetUpdate.updateTime != 0, 563 | "Pending validator set update already finalized" 564 | ); 565 | 566 | uint32 errorCode = getDisputePeriodErrorCode( 567 | pendingValidatorSetUpdate.updateTime, 568 | pendingValidatorSetUpdate.updateBlockNumber 569 | ); 570 | require(errorCode == 0, "Still in dispute period"); 571 | 572 | finalizeValidatorSetUpdateInner(); 573 | } 574 | 575 | function finalizeValidatorSetUpdateInner() private { 576 | hotValidatorSetHash = pendingValidatorSetUpdate.hotValidatorSetHash; 577 | coldValidatorSetHash = pendingValidatorSetUpdate.coldValidatorSetHash; 578 | epoch = pendingValidatorSetUpdate.epoch; 579 | totalValidatorPower = pendingValidatorSetUpdate.totalValidatorPower; 580 | nValidators = pendingValidatorSetUpdate.nValidators; 581 | pendingValidatorSetUpdate.updateTime = 0; 582 | 583 | emit FinalizedValidatorSetUpdate( 584 | epoch, 585 | pendingValidatorSetUpdate.hotValidatorSetHash, 586 | pendingValidatorSetUpdate.coldValidatorSetHash 587 | ); 588 | } 589 | 590 | function makeMessage(bytes32 data) private view returns (bytes32) { 591 | Agent memory agent = Agent("a", keccak256(abi.encode(address(this), data))); 592 | return hash(agent); 593 | } 594 | 595 | function modifyLocker( 596 | address locker, 597 | bool _isLocker, 598 | uint64 nonce, 599 | ValidatorSet calldata activeValidatorSet, 600 | Signature[] memory signatures 601 | ) external { 602 | bytes32 data = keccak256(abi.encode("modifyLocker", locker, _isLocker, nonce)); 603 | bytes32 message = makeMessage(data); 604 | 605 | bytes32 validatorSetHash; 606 | if (_isLocker) { 607 | validatorSetHash = hotValidatorSetHash; 608 | } else { 609 | validatorSetHash = coldValidatorSetHash; 610 | } 611 | 612 | checkMessageNotUsed(message); 613 | checkValidatorSignatures(message, activeValidatorSet, signatures, validatorSetHash); 614 | if (lockers[locker] && !_isLocker && !paused()) { 615 | removeLockerVote(locker); 616 | } 617 | lockers[locker] = _isLocker; 618 | emit ModifiedLocker(locker, _isLocker); 619 | } 620 | 621 | function modifyFinalizer( 622 | address finalizer, 623 | bool _isFinalizer, 624 | uint64 nonce, 625 | ValidatorSet calldata activeValidatorSet, 626 | Signature[] memory signatures 627 | ) external { 628 | bytes32 data = keccak256(abi.encode("modifyFinalizer", finalizer, _isFinalizer, nonce)); 629 | bytes32 message = makeMessage(data); 630 | 631 | bytes32 validatorSetHash; 632 | if (_isFinalizer) { 633 | validatorSetHash = hotValidatorSetHash; 634 | } else { 635 | validatorSetHash = coldValidatorSetHash; 636 | } 637 | 638 | checkMessageNotUsed(message); 639 | checkValidatorSignatures(message, activeValidatorSet, signatures, validatorSetHash); 640 | finalizers[finalizer] = _isFinalizer; 641 | emit ModifiedFinalizer(finalizer, _isFinalizer); 642 | } 643 | 644 | function checkFinalizer(address finalizer) private view { 645 | require(finalizers[finalizer], "Sender is not a finalizer"); 646 | } 647 | 648 | // This function checks that the total power of the new validator set is greater than zero. 649 | function checkNewValidatorPowers(uint64[] memory powers) private pure returns (uint64) { 650 | uint64 cumulativePower; 651 | for (uint64 i; i < powers.length; i++) { 652 | cumulativePower += powers[i]; 653 | } 654 | 655 | require(cumulativePower > 0, "Submitted validator powers must be greater than zero"); 656 | return cumulativePower; 657 | } 658 | 659 | function changeDisputePeriodSeconds( 660 | uint64 newDisputePeriodSeconds, 661 | uint64 nonce, 662 | ValidatorSet memory activeColdValidatorSet, 663 | Signature[] memory signatures 664 | ) external { 665 | bytes32 data = keccak256( 666 | abi.encode("changeDisputePeriodSeconds", newDisputePeriodSeconds, nonce) 667 | ); 668 | bytes32 message = makeMessage(data); 669 | checkMessageNotUsed(message); 670 | checkValidatorSignatures(message, activeColdValidatorSet, signatures, coldValidatorSetHash); 671 | 672 | disputePeriodSeconds = newDisputePeriodSeconds; 673 | emit ChangedDisputePeriodSeconds(newDisputePeriodSeconds); 674 | } 675 | 676 | function invalidateWithdrawals( 677 | bytes32[] memory messages, 678 | uint64 nonce, 679 | ValidatorSet memory activeColdValidatorSet, 680 | Signature[] memory signatures 681 | ) external { 682 | bytes32 data = keccak256(abi.encode("invalidateWithdrawals", messages, nonce)); 683 | bytes32 message = makeMessage(data); 684 | 685 | checkMessageNotUsed(message); 686 | checkValidatorSignatures(message, activeColdValidatorSet, signatures, coldValidatorSetHash); 687 | 688 | uint64 end = uint64(messages.length); 689 | for (uint64 idx; idx < end; idx++) { 690 | withdrawalsInvalidated[messages[idx]] = true; 691 | emit InvalidatedWithdrawal(requestedWithdrawals[messages[idx]]); 692 | } 693 | } 694 | 695 | function changeBlockDurationMillis( 696 | uint64 newBlockDurationMillis, 697 | uint64 nonce, 698 | ValidatorSet memory activeColdValidatorSet, 699 | Signature[] memory signatures 700 | ) external { 701 | bytes32 data = keccak256( 702 | abi.encode("changeBlockDurationMillis", newBlockDurationMillis, nonce) 703 | ); 704 | bytes32 message = makeMessage(data); 705 | 706 | checkMessageNotUsed(message); 707 | checkValidatorSignatures(message, activeColdValidatorSet, signatures, coldValidatorSetHash); 708 | 709 | blockDurationMillis = newBlockDurationMillis; 710 | emit ChangedBlockDurationMillis(newBlockDurationMillis); 711 | } 712 | 713 | function changeLockerThreshold( 714 | uint64 newLockerThreshold, 715 | uint64 nonce, 716 | ValidatorSet memory activeColdValidatorSet, 717 | Signature[] memory signatures 718 | ) external { 719 | bytes32 data = keccak256(abi.encode("changeLockerThreshold", newLockerThreshold, nonce)); 720 | bytes32 message = makeMessage(data); 721 | 722 | checkMessageNotUsed(message); 723 | checkValidatorSignatures(message, activeColdValidatorSet, signatures, coldValidatorSetHash); 724 | 725 | lockerThreshold = newLockerThreshold; 726 | if (uint64(lockersVotingLock.length) >= lockerThreshold && !paused()) { 727 | _pause(); 728 | } 729 | emit ChangedLockerThreshold(newLockerThreshold); 730 | } 731 | 732 | function getLockersVotingLock() external view returns (address[] memory) { 733 | return lockersVotingLock; 734 | } 735 | 736 | function isVotingLock(address locker) public view returns (bool) { 737 | uint64 length = uint64(lockersVotingLock.length); 738 | for (uint64 i = 0; i < length; i++) { 739 | if (lockersVotingLock[i] == locker) { 740 | return true; 741 | } 742 | } 743 | return false; 744 | } 745 | 746 | function voteEmergencyLock() external { 747 | require(lockers[msg.sender], "Sender is not authorized to lock smart contract"); 748 | require(!isVotingLock(msg.sender), "Locker already voted for emergency lock"); 749 | lockersVotingLock.push(msg.sender); 750 | if (uint64(lockersVotingLock.length) >= lockerThreshold && !paused()) { 751 | _pause(); 752 | } 753 | } 754 | 755 | function unvoteEmergencyLock() external whenNotPaused { 756 | require(lockers[msg.sender], "Sender is not authorized to lock smart contract"); 757 | require(isVotingLock(msg.sender), "Locker is not currently voting for emergency lock"); 758 | removeLockerVote(msg.sender); 759 | } 760 | 761 | function removeLockerVote(address locker) private whenNotPaused { 762 | require(lockers[locker], "Address is not authorized to lock smart contract"); 763 | uint64 length = uint64(lockersVotingLock.length); 764 | for (uint64 i = 0; i < length; i++) { 765 | if (lockersVotingLock[i] == locker) { 766 | lockersVotingLock[i] = lockersVotingLock[length - 1]; 767 | lockersVotingLock.pop(); 768 | break; 769 | } 770 | } 771 | } 772 | 773 | function emergencyUnlock( 774 | ValidatorSetUpdateRequest memory newValidatorSet, 775 | ValidatorSet calldata activeColdValidatorSet, 776 | Signature[] calldata signatures, 777 | uint64 nonce 778 | ) external whenPaused { 779 | bytes32 data = keccak256( 780 | abi.encode( 781 | "unlock", 782 | newValidatorSet.epoch, 783 | newValidatorSet.hotAddresses, 784 | newValidatorSet.coldAddresses, 785 | newValidatorSet.powers, 786 | nonce 787 | ) 788 | ); 789 | bytes32 message = makeMessage(data); 790 | 791 | checkMessageNotUsed(message); 792 | updateValidatorSetInner(newValidatorSet, activeColdValidatorSet, signatures, message, true); 793 | finalizeValidatorSetUpdateInner(); 794 | delete lockersVotingLock; 795 | _unpause(); 796 | } 797 | 798 | function depositWithPermit( 799 | address user, 800 | uint64 usd, 801 | uint64 deadline, 802 | Signature memory signature 803 | ) private { 804 | address spender = address(this); 805 | try 806 | usdcToken.permit( 807 | user, 808 | spender, 809 | usd, 810 | deadline, 811 | signature.v, 812 | bytes32(signature.r), 813 | bytes32(signature.s) 814 | ) 815 | {} catch { 816 | emit FailedPermitDeposit(user, usd, 0); 817 | return; 818 | } 819 | 820 | try usdcToken.transferFrom(user, spender, usd) returns (bool success) { 821 | if (!success) { 822 | emit FailedPermitDeposit(user, usd, 1); 823 | } 824 | } catch { 825 | emit FailedPermitDeposit(user, usd, 1); 826 | } 827 | } 828 | 829 | function batchedDepositWithPermit( 830 | DepositWithPermit[] memory deposits 831 | ) external nonReentrant whenNotPaused { 832 | uint64 end = uint64(deposits.length); 833 | for (uint64 idx; idx < end; idx++) { 834 | depositWithPermit( 835 | deposits[idx].user, 836 | deposits[idx].usd, 837 | deposits[idx].deadline, 838 | deposits[idx].signature 839 | ); 840 | } 841 | } 842 | } 843 | -------------------------------------------------------------------------------- /Signature.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.9; 3 | 4 | struct Agent { 5 | string source; 6 | bytes32 connectionId; 7 | } 8 | 9 | struct Signature { 10 | uint256 r; 11 | uint256 s; 12 | uint8 v; 13 | } 14 | 15 | bytes32 constant EIP712_DOMAIN_SEPARATOR = keccak256( 16 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 17 | ); 18 | 19 | bytes32 constant AGENT_TYPEHASH = keccak256("Agent(string source,bytes32 connectionId)"); 20 | 21 | address constant VERIFYING_CONTRACT = address(0); 22 | 23 | function hash(Agent memory agent) pure returns (bytes32) { 24 | return keccak256(abi.encode(AGENT_TYPEHASH, keccak256(bytes(agent.source)), agent.connectionId)); 25 | } 26 | 27 | function makeDomainSeparator() view returns (bytes32) { 28 | return 29 | keccak256( 30 | abi.encode( 31 | EIP712_DOMAIN_SEPARATOR, 32 | keccak256(bytes("Exchange")), 33 | keccak256(bytes("1")), 34 | block.chainid, 35 | VERIFYING_CONTRACT 36 | ) 37 | ); 38 | } 39 | 40 | function recoverSigner( 41 | bytes32 dataHash, 42 | Signature memory sig, 43 | bytes32 domainSeparator 44 | ) pure returns (address) { 45 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, dataHash)); 46 | address signerRecovered = ecrecover(digest, sig.v, bytes32(sig.r), bytes32(sig.s)); 47 | require(signerRecovered != address(0), "Invalid signature, recovered the zero address"); 48 | 49 | return signerRecovered; 50 | } 51 | -------------------------------------------------------------------------------- /tests/bridge_watcher2.rs: -------------------------------------------------------------------------------- 1 | // This task is run asynchronously by each node process. 2 | // The task listens to events emitted by the Arbitrum contract and sends relevant L1 signatures and 3 | // Arbitrum finalization transactions. 4 | 5 | use crate::prelude::*; 6 | use crate::{ 7 | action::{ 8 | sign_validator_set_update::SignValidatorSetUpdateAction, 9 | vote_eth_claimed_withdrawal::VoteEthRequestedWithdrawalAction, vote_eth_deposit::VoteEthDepositAction, 10 | vote_eth_finalized_validator_set_update::VoteEthFinalizedValidatorSetUpdateAction, 11 | vote_eth_finalized_withdrawal::VoteEthFinalizedWithdrawalAction, 12 | vote_eth_validator_set_update::VoteEthValidatorSetUpdateAction, ValidatorSignWithdrawalAction, 13 | }, 14 | bridge2::{ 15 | finalize_validator_set_update, Bridge2, PendingValidatorSetUpdate, SolValidatorSet, SolValidatorSetUpdate, 16 | UserAndNonce, Withdrawal, 17 | }, 18 | bridge_watcher::query_tx_receipt, 19 | etherscan_tx_tracker::EtherscanTxTracker, 20 | staking::Staking, 21 | }; 22 | 23 | #[derive(Debug)] 24 | pub(crate) enum Event { 25 | Deposit(DepositEvent), 26 | RequestedWithdrawal(RequestedWithdrawalEvent), 27 | FinalizedWithdrawal(FinalizedWithdrawalEvent), 28 | RequestedValidatorSetUpdate(RequestedValidatorSetUpdateEvent), 29 | FinalizedValidatorSetUpdate(FinalizedValidatorSetUpdateEvent), 30 | } 31 | 32 | #[derive(Clone, Copy, EnumIter)] 33 | enum EventType { 34 | Deposit, 35 | RequestedWithdrawal, 36 | FinalizedWithdrawal, 37 | RequestedValidatorSetUpdate, 38 | FinalizedValidatorSetUpdate, 39 | } 40 | 41 | #[derive(Debug, EthAbiType)] 42 | pub(crate) struct DepositEvent { 43 | pub(crate) user: H160, 44 | pub(crate) usdc: u64, 45 | } 46 | 47 | #[derive(Debug, EthAbiType)] 48 | pub(crate) struct RequestedWithdrawalEvent { 49 | pub(crate) user: H160, 50 | pub(crate) usdc: u64, 51 | pub(crate) nonce: u64, 52 | pub(crate) message: H256, 53 | pub(crate) requested_time: u64, 54 | pub(crate) block_number: u64, 55 | } 56 | 57 | #[derive(Debug, EthAbiType)] 58 | pub(crate) struct FinalizedWithdrawalEvent { 59 | pub(crate) user: H160, 60 | pub(crate) usdc: u64, 61 | pub(crate) nonce: u64, 62 | pub(crate) message: H256, 63 | } 64 | 65 | #[derive(Debug, EthAbiType)] 66 | pub(crate) struct RequestedValidatorSetUpdateEvent { 67 | pub(crate) epoch: Epoch, 68 | pub(crate) hot_validator_set_hash: H256, 69 | pub(crate) cold_validator_set_hash: H256, 70 | pub(crate) update_time: u64, 71 | pub(crate) block_number: u64, 72 | } 73 | 74 | #[derive(Debug, EthAbiType)] 75 | pub(crate) struct FinalizedValidatorSetUpdateEvent { 76 | pub(crate) epoch: Epoch, 77 | pub(crate) hot_validator_set_hash: H256, 78 | pub(crate) cold_validator_set_hash: H256, 79 | } 80 | 81 | #[derive(Debug)] 82 | #[cfg_attr(test, derive(PartialEq))] 83 | struct ValidatorSetUpdateArgs { 84 | sol_new_validator_set: SolValidatorSetUpdate, 85 | sol_cur_validator_set: SolValidatorSet, 86 | signers: Vec, 87 | signatures: Vec, 88 | } 89 | 90 | pub(crate) async fn spawn(replicator: Arc, tx_batcher: Arc, should_finalize: bool) { 91 | utils::tokio_spawn_forever("BridgeWatcher2", async move { 92 | let chain = replicator.lock(|e| e.chain(), "bw2_chain"); 93 | let client = chain.eth_client(Nickname::Owner).await; 94 | 95 | let priv_validator_key_file = if_test!( 96 | "golden_inputs/priv_validator_key.json".to_string(), 97 | format!("{}/code/hyperliquid/data/tendermint/priv_validator_key.json", C.cham_dir) 98 | ); 99 | let signing_key = utils::ed25519_signing_key(&priv_validator_key_file); 100 | let validator: H256 = signing_key.verification_key().to_bytes().into(); 101 | let validator_wallet = utils::wallet(validator); 102 | let mut l1_actions_time_stream = lu::timer_stream(Duration(if_test!(0.5, 2.)), None); 103 | let mut eth_actions_time_stream = lu::timer_stream(Duration(if_test!(0.5, 30.)), None); 104 | 105 | let dispute_period_millis = dispute_period_millis(&client, chain).await; 106 | let block_duration_millis = chain.bridge2_cid().call("blockDurationMillis", (), &client).await.unwrap(); 107 | 108 | let eth_chain = chain.eth_chain(); 109 | let bridge_address = chain.bridge2_cid().address(eth_chain); 110 | let etherscan_tx_tracker = EtherscanTxTracker::new(chain, bridge_address); 111 | loop { 112 | let validator_active = replicator.lock(|e| e.staking().validator_active(validator), "bw2_validator_active"); 113 | if validator_active { 114 | break; 115 | } 116 | error!("BridgeWatcher2: Owner is not a registered validator." => validator); 117 | lu::async_sleep(Duration(5.)).await; 118 | } 119 | 120 | loop { 121 | tokio::select! { 122 | Some(_) = l1_actions_time_stream.next() => { 123 | let txs = etherscan_tx_tracker.lock().txs(eth_chain, bridge_address).clone(); 124 | let bridge2 = replicator.lock(|e| e.bridge2().clone(), "bw2_bridge2"); 125 | if let Err(err) = 126 | register_eth_events_on_l1(chain, &client, &bridge2, &tx_batcher, &validator_wallet, &txs).await 127 | { 128 | // NOSHIP investigate why this errors but not in bridge_watcher 129 | error!("Error running register_eth_events_on_l1: {err}"); 130 | } 131 | 132 | if let Err(err) = sign_l1_actions(&bridge2, &tx_batcher, &validator_wallet, chain, validator).await { 133 | error!("Error running sign_l1_actions: {err}"); 134 | } 135 | } 136 | 137 | Some(_) = eth_actions_time_stream.next() => { 138 | let pending_validator_set_update: PendingValidatorSetUpdate = 139 | chain.bridge2_cid().call("pendingValidatorSetUpdate", (), &client).await.unwrap(); 140 | let (bridge2, staking) = replicator.lock(|e| (e.bridge2().clone(), e.staking().clone()), "bw2_bridge2_and_staking"); 141 | 142 | if let Err(err) = 143 | maybe_update_validator_set(&bridge2, &staking, &client, chain, pending_validator_set_update.clone()).await 144 | { 145 | error!("Error running maybe_update_validator_set: {err}"); 146 | } 147 | 148 | if should_finalize { 149 | if let Err(err) = maybe_finalize_validator_set_update(&client, chain, pending_validator_set_update, dispute_period_millis, block_duration_millis).await { 150 | error!("Error running maybe_finalize_validator_set_update: {err}"); 151 | } 152 | 153 | if let Err(err) = batch_finalize_withdrawals(&bridge2, &client, chain, dispute_period_millis, block_duration_millis).await { 154 | error!("Error running batch_finalize_withdrawals: {err}"); 155 | } 156 | } 157 | } 158 | } 159 | } 160 | }); 161 | } 162 | 163 | async fn register_eth_events_on_l1( 164 | chain: Chain, 165 | client: &EthClient, 166 | bridge2: &Bridge2, 167 | tx_batcher: &TxBatcher, 168 | owner_wallet: &Wallet, 169 | txs: &Set, 170 | ) -> infra::Result { 171 | let events = etherscan_events(chain, client, txs).await?; 172 | 173 | let mut last_seen_block = 0; 174 | let processed_deposits = bridge2.processed_deposits().clone(); 175 | for (eth_id, (event, tx_receipt)) in events { 176 | let cur_block = tx_receipt.block_number.unwrap().as_u64(); 177 | last_seen_block = last_seen_block.max(cur_block); 178 | if !processed_deposits.contains(ð_id) { 179 | let action: Box = match event { 180 | Event::Deposit(DepositEvent { user, usdc, .. }) => Box::new(VoteEthDepositAction { 181 | user: user.into(), 182 | usd: usdc, 183 | eth_id, 184 | eth_tx_hash: tx_receipt.transaction_hash, 185 | }), 186 | Event::RequestedWithdrawal(RequestedWithdrawalEvent { 187 | user, 188 | nonce, 189 | requested_time, 190 | usdc, 191 | block_number, 192 | .. 193 | }) => Box::new(VoteEthRequestedWithdrawalAction { 194 | user: user.into(), 195 | usd: usdc, 196 | nonce, 197 | requested_time: Time::from_unix_millis(requested_time * 1000).unwrap(), 198 | block_number, 199 | }), 200 | Event::FinalizedWithdrawal(FinalizedWithdrawalEvent { user, nonce, usdc, .. }) => { 201 | Box::new(VoteEthFinalizedWithdrawalAction { user: user.into(), nonce, usd: usdc }) 202 | } 203 | Event::RequestedValidatorSetUpdate(RequestedValidatorSetUpdateEvent { 204 | epoch, 205 | hot_validator_set_hash, 206 | cold_validator_set_hash, 207 | update_time, 208 | block_number, 209 | }) => { 210 | let validator_set_hash = utils::keccak((hot_validator_set_hash, cold_validator_set_hash)); 211 | Box::new(VoteEthValidatorSetUpdateAction { 212 | epoch, 213 | update_time: Time::from_unix_millis(update_time * 1000).unwrap(), 214 | validator_set_hash, 215 | block_number, 216 | }) 217 | } 218 | Event::FinalizedValidatorSetUpdate(FinalizedValidatorSetUpdateEvent { 219 | epoch, 220 | hot_validator_set_hash, 221 | cold_validator_set_hash, 222 | }) => { 223 | let validator_set_hash = utils::keccak((hot_validator_set_hash, cold_validator_set_hash)); 224 | Box::new(VoteEthFinalizedValidatorSetUpdateAction { epoch, validator_set_hash }) 225 | } 226 | }; 227 | let signed_action = SignedAction::new(action, owner_wallet)?; 228 | warn!("register_eth_events_on_l1" => signed_action, owner_wallet); 229 | tx_batcher.send_signed_action(signed_action).await?; 230 | } else { 231 | warn!("Event already processed" => event); 232 | } 233 | } 234 | 235 | let _cur_block = client.cur_block_number().await.unwrap(); 236 | Ok(last_seen_block.min(if_test!(0, _cur_block - 10_000))) 237 | } 238 | 239 | async fn sign_l1_actions( 240 | bridge2: &Bridge2, 241 | tx_batcher: &TxBatcher, 242 | owner_wallet: &Wallet, 243 | chain: Chain, 244 | validator: Validator, 245 | ) -> Result<()> { 246 | let withdrawals_to_sign = bridge2.withdrawal_signatures().without_validator_vote(validator); 247 | let validator_sets_to_sign = bridge2.validator_set_signatures().without_validator_vote(validator); 248 | 249 | for (&UserAndNonce { user, nonce }, value_to_withdrawal) in withdrawals_to_sign.iter() { 250 | for &usd in value_to_withdrawal.keys() { 251 | let hash = utils::keccak((user.raw(), usd, nonce)); 252 | let signature = chain.sign_phantom_agent(hash, owner_wallet); 253 | let action = Box::new(ValidatorSignWithdrawalAction { user, usd, nonce, signature }); 254 | let req = SignedAction::new(action, owner_wallet).unwrap(); 255 | tx_batcher.send_signed_action(req).await?; 256 | } 257 | } 258 | 259 | for (epoch, hash_to_signatures) in validator_sets_to_sign { 260 | for &validator_set_hash in hash_to_signatures.keys() { 261 | let signature = chain.sign_phantom_agent(validator_set_hash, owner_wallet); 262 | let action = Box::new(SignValidatorSetUpdateAction { epoch, validator_set_hash, signature }); 263 | let req = SignedAction::new(action, owner_wallet).unwrap(); 264 | tx_batcher.send_signed_action(req).await?; 265 | } 266 | } 267 | 268 | Ok(()) 269 | } 270 | 271 | // NOSHIP move this into EtherscanTxTracker and don't load the files over and over again 272 | async fn etherscan_events( 273 | chain: Chain, 274 | client: &EthClient, 275 | txs: &Set, 276 | ) -> infra::Result> { 277 | let start = Instant::now(); 278 | let cid = chain.bridge2_cid(); 279 | let mut res = Map::new(); 280 | for event_type in EventType::iter() { 281 | for tx in txs.clone() { 282 | let receipt = query_tx_receipt(tx, client).await?; 283 | 284 | let events: Vec<_> = match event_type { 285 | EventType::Deposit => { 286 | client.parse_events(cid, receipt.clone()).into_iter().map(Event::Deposit).collect() 287 | } 288 | EventType::RequestedWithdrawal => { 289 | client.parse_events(cid, receipt.clone()).into_iter().map(Event::RequestedWithdrawal).collect() 290 | } 291 | EventType::FinalizedWithdrawal => { 292 | client.parse_events(cid, receipt.clone()).into_iter().map(Event::FinalizedWithdrawal).collect() 293 | } 294 | EventType::RequestedValidatorSetUpdate => client 295 | .parse_events(cid, receipt.clone()) 296 | .into_iter() 297 | .map(Event::RequestedValidatorSetUpdate) 298 | .collect(), 299 | EventType::FinalizedValidatorSetUpdate => client 300 | .parse_events(cid, receipt.clone()) 301 | .into_iter() 302 | .map(Event::FinalizedValidatorSetUpdate) 303 | .collect(), 304 | }; 305 | for (pos, event) in events.into_iter().enumerate() { 306 | let hash = utils::keccak((pos as u32, tx)); 307 | res.insert(hash, (event, receipt.clone())); 308 | } 309 | } 310 | } 311 | warn!("parsed all events from etherscan txs" => cid, res.len(), u::profile(start)); 312 | Ok(res) 313 | } 314 | 315 | fn validator_set_ready( 316 | bridge2: &Bridge2, 317 | staking: &Staking, 318 | pending_validator_set_update: PendingValidatorSetUpdate, 319 | ) -> Option { 320 | let validator_sets_ready = bridge2.validator_sets_ready(); 321 | let active_epoch = staking.active_epoch(); 322 | let PendingValidatorSetUpdate { epoch: pending_epoch, .. } = pending_validator_set_update; 323 | if let Some((&epoch, validator_set_and_signatures)) = validator_sets_ready.iter().rev().next() { 324 | if epoch <= active_epoch || epoch <= pending_epoch.as_u64() { 325 | return None; 326 | } 327 | let cur_validator_set = staking.validator_set(active_epoch).unwrap(); 328 | let sol_cur_validator_set = SolValidatorSet::from_hot_validator_set(epoch, &cur_validator_set); 329 | let new_validator_set: Set<_> = validator_set_and_signatures.keys().copied().collect(); 330 | let sol_new_validator_set = SolValidatorSetUpdate::from_validator_set(epoch, &new_validator_set); 331 | let mut validator_set_and_signatures: Vec<_> = validator_set_and_signatures.iter().collect(); 332 | validator_set_and_signatures.sort_by_key(|(_, x)| Reverse(x.power)); 333 | let mut signers = Vec::new(); 334 | let mut signatures = Vec::new(); 335 | for (&validator_profile, validator_signature) in validator_set_and_signatures { 336 | signers.push(validator_profile.hot_user.raw()); 337 | signatures.push(validator_signature.clone().signature); 338 | } 339 | 340 | let validator_set_update_args = 341 | ValidatorSetUpdateArgs { sol_new_validator_set, sol_cur_validator_set, signers, signatures }; 342 | return Some(validator_set_update_args); 343 | } 344 | None 345 | } 346 | 347 | async fn maybe_update_validator_set( 348 | bridge2: &Bridge2, 349 | staking: &Staking, 350 | client: &EthClient, 351 | chain: Chain, 352 | pending_validator_set_update: PendingValidatorSetUpdate, 353 | ) -> infra::Result<()> { 354 | if let Some(new_validator_set_args) = validator_set_ready(bridge2, staking, pending_validator_set_update) { 355 | warn!("bridge_watcher2: sending new validator set update request" => new_validator_set_args); 356 | let ValidatorSetUpdateArgs { sol_new_validator_set, sol_cur_validator_set, signers, signatures } = 357 | new_validator_set_args; 358 | chain 359 | .bridge2_cid() 360 | .send("updateValidatorSet", (sol_new_validator_set, sol_cur_validator_set, signers, signatures), client) 361 | .await?; 362 | } 363 | Ok(()) 364 | } 365 | 366 | async fn batch_finalize_withdrawals( 367 | bridge2: &Bridge2, 368 | client: &EthClient, 369 | chain: Chain, 370 | dispute_period_millis: u64, 371 | block_duration_millis: u64, 372 | ) -> infra::Result<()> { 373 | let withdrawals_to_finalize = bridge2.withdrawals_to_finalize().clone(); 374 | let mut messages = Vec::new(); 375 | let time = InfraTime::wall_clock_now(); 376 | let cur_block_number = client.cur_block_number().await.unwrap(); 377 | for (user_and_nonce, usd_and_time) in withdrawals_to_finalize { 378 | let Withdrawal { usd, requested_time, block_number } = usd_and_time; 379 | let enough_time_passed = requested_time.to_unix_millis() + dispute_period_millis > time.to_unix_millis(); 380 | let enough_blocks_passed = (cur_block_number - block_number) * block_duration_millis > dispute_period_millis; 381 | if enough_time_passed && enough_blocks_passed { 382 | let UserAndNonce { user, nonce } = user_and_nonce; 383 | let message = utils::keccak((user.raw(), usd, nonce)); 384 | messages.push(message); 385 | } 386 | } 387 | 388 | let batches: Vec<_> = messages.chunks(200).collect(); 389 | for batch in batches { 390 | let batch = batch.to_vec(); 391 | chain.bridge2_cid().send("batchedFinalizeWithdrawals", batch, client).await?; 392 | } 393 | 394 | Ok(()) 395 | } 396 | 397 | async fn maybe_finalize_validator_set_update( 398 | client: &EthClient, 399 | chain: Chain, 400 | pending_validator_set_update: PendingValidatorSetUpdate, 401 | dispute_period_millis: u64, 402 | block_duration_millis: u64, 403 | ) -> infra::Result<()> { 404 | let update_time_millis = pending_validator_set_update.update_time.as_u64() * 1000; 405 | if update_time_millis == 0 { 406 | warn!("pending validator set update already finalized" => pending_validator_set_update); 407 | return Ok(()); 408 | } 409 | 410 | let cur_time = InfraTime::wall_clock_now(); 411 | let cur_block_number = client.cur_block_number().await.unwrap(); 412 | let update_block_number = pending_validator_set_update.update_block_number.as_u64(); 413 | let not_enough_time_passed = cur_time.to_unix_millis() < update_time_millis + dispute_period_millis; 414 | let not_enough_blocks_passed = 415 | (cur_block_number - update_block_number) * block_duration_millis < dispute_period_millis; 416 | if not_enough_time_passed || not_enough_blocks_passed { 417 | warn!("pending validator set update still in dispute period" => pending_validator_set_update); 418 | return Ok(()); 419 | } 420 | finalize_validator_set_update(client, chain).await?; 421 | Ok(()) 422 | } 423 | 424 | async fn dispute_period_millis(client: &EthClient, chain: Chain) -> u64 { 425 | let dispute_period_seconds: u64 = chain.bridge2_cid().call("disputePeriodSeconds", (), client).await.unwrap(); 426 | dispute_period_seconds * 1000 427 | } 428 | 429 | #[cfg(test)] 430 | mod test { 431 | 432 | use infra::set; 433 | 434 | use super::*; 435 | use crate::{ 436 | action::sign_validator_set_update::SignValidatorSetUpdateAction, 437 | bridge2::{make_validator_set_hash, validator_set_hashes, ValidatorProfile, ValidatorSignature}, 438 | }; 439 | 440 | #[test] 441 | fn pending_validator_sets_ready_test() { 442 | let chain: Chain = Chain::Local; 443 | let mut staking = Staking::new(); 444 | let mut bridge2 = Bridge2::new(); 445 | let validator = tu::main_validator(); 446 | let validator_wallet = utils::wallet(tu::main_validator()); 447 | let hot_user = tu::main_validator_hot_user(); 448 | let cold_user = tu::main_validator_cold_user(); 449 | 450 | staking.maybe_increase_epoch(Time::from_unix_millis(1_000_000).unwrap()); 451 | staking.maybe_increase_epoch(Time::from_unix_millis(2_000_000).unwrap()); 452 | 453 | let new_active_epoch = 1; 454 | let validator_set = set![ValidatorProfile { power: 1, hot_user, cold_user }]; 455 | let hash = make_validator_set_hash(new_active_epoch, validator_set.clone()); 456 | let signature = chain.sign_phantom_agent(hash, &validator_wallet); 457 | let validator_signatures = map!(validator => ValidatorSignature { signature: signature.clone(), power: 1 }); 458 | let sign_validator_set_update_action = SignValidatorSetUpdateAction { 459 | epoch: new_active_epoch, 460 | validator_set_hash: hash, 461 | signature: signature.clone(), 462 | }; 463 | bridge2.sign_validator_set_update(chain, hot_user, sign_validator_set_update_action, &staking).unwrap(); 464 | assert_eq!( 465 | bridge2.validator_set_signatures().validators_and_signatures(new_active_epoch, hash).unwrap(), 466 | &validator_signatures 467 | ); 468 | 469 | let (hot_validator_set_hash, cold_validator_set_hash) = validator_set_hashes(new_active_epoch, validator_set); 470 | let pending_validator_set_update = PendingValidatorSetUpdate { 471 | epoch: U256::zero(), 472 | total_validator_power: U256::one(), 473 | update_time: U256::one(), 474 | update_block_number: U256::zero(), 475 | hot_validator_set_hash, 476 | cold_validator_set_hash, 477 | n_validators: U256::one(), 478 | }; 479 | 480 | let res = validator_set_ready(&bridge2, &staking, pending_validator_set_update).unwrap(); 481 | let powers = vec![U256::one()]; 482 | assert_eq!( 483 | res, 484 | ValidatorSetUpdateArgs { 485 | sol_new_validator_set: SolValidatorSetUpdate { 486 | epoch: new_active_epoch.into(), 487 | hot_addresses: vec![hot_user.raw()], 488 | cold_addresses: vec![cold_user.raw()], 489 | powers: powers.clone(), 490 | }, 491 | sol_cur_validator_set: SolValidatorSet { 492 | epoch: new_active_epoch.into(), 493 | validators: vec![hot_user.raw()], 494 | powers, 495 | }, 496 | signers: vec![hot_user.raw()], 497 | signatures: vec![signature] 498 | } 499 | ); 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /tests/example.rs: -------------------------------------------------------------------------------- 1 | // This integration test spins up local tendermint, abci, and hardhat servers. 2 | // 3 | // NOTE: This does not compile without all Chameleon Trading rust dependencies. 4 | // 5 | // run_bridge_watcher monitors the old bridge that will be phased out 6 | // run_bridge_watcher2 monitors the current bridge being audited 7 | // Replicator, HyperAbci, *Action, and other unexplained things relate to the L1, 8 | // so please take those as black boxes that work. 9 | // 10 | // The most relevant tests that isolate the bridge being audited are the bridge2_* tests. 11 | // 12 | // Note that bridge_watcher2 is a task that polls for events emitted by Bridge2 and sends the 13 | // required validator set update and finalization transactions. 14 | // See Bridge2.sol and tests/bridge_watcher2.rs for details. 15 | 16 | use crate::prelude::*; 17 | use crate::{ 18 | abci_state::AbciStateBuilder, 19 | action::{owner_mint_token_for::OwnerMintTokenForAction, token_delegate::TokenDelegateAction}, 20 | bridge2::{ 21 | finalize_validator_set_update, sign_and_update_validator_set, SolValidatorSet, SolValidatorSetUpdate, 22 | ValidatorProfile, WithdrawalVoucher2, 23 | }, 24 | bridge_watcher2::spawn as spawn_bridge_watcher2, 25 | cancel_response::{FCancelResponse, FCancelStatus}, 26 | etherscan_tx_tracker::TXS, 27 | hyper_abci::{HyperAbci, ReplicaAbciCmd}, 28 | order_response::{FOrderResponse, FOrderStatus}, 29 | replicator::DbInit, 30 | run_web_server::WebServerConfig, 31 | signed_action::FSignedActionSuccess, 32 | tendermint_init::{TendermintHome, TendermintInit}, 33 | }; 34 | use ethers::prelude::k256::ecdsa::SigningKey; 35 | use infra::{set, shell}; 36 | use std::sync::atomic::AtomicBool; 37 | use ethers::abi::Tokenizable; 38 | 39 | #[tokio::test] 40 | async fn integration_tests() { 41 | if tu::ci() { 42 | return; 43 | } 44 | let recver = setup().await; 45 | EthClient::skip_api_validation(); 46 | 47 | let chain = Chain::Local; 48 | let http_client = Arc::new(HttpClient::new(Some(ApiUrl::localhost(3002)))); 49 | let owner_eth_client = chain.eth_client(Nickname::Owner).await; 50 | let user_eth_client = chain.eth_client(Nickname::User).await; 51 | owner_eth_client.send_eth(user_eth_client.address(), 1.).await.unwrap(); 52 | let user_eth_client = chain.eth_client(Nickname::User).await; 53 | owner_eth_client.send_eth(user_eth_client.address(), 1.).await.unwrap(); 54 | owner_eth_client.send_eth(tu::main_validator_hot_user().to_address(), 1.).await.unwrap(); 55 | 56 | let db_hub = DbHub::test(); 57 | let replicator = Arc::new( 58 | ReplicatorBuilder::new( 59 | db_hub.clone(), 60 | DbInit::Legacy, 61 | AbciStateBuilder::New { chain }.build(), 62 | Some(tu::aux_core()), 63 | Ipv4Addr::LOCALHOST, 64 | recver, 65 | ) 66 | .build() 67 | .await, 68 | ); 69 | 70 | { 71 | let replicator = Arc::clone(&replicator); 72 | tokio::spawn(async move { 73 | setup_web_server(db_hub, replicator).await; 74 | }); 75 | } 76 | let tx_batcher = Arc::new(TxBatcher::new(Ipv4Addr::LOCALHOST).await); 77 | spawn_bridge_watcher2(replicator.abci_state(), tx_batcher, true).await; 78 | 79 | lu::async_sleep(Duration(1.)).await; 80 | l1_test(&http_client).await; 81 | bridge_end_to_end_test(&http_client, &replicator).await; 82 | bridge2_end_to_end_test(&http_client, &replicator).await; 83 | bridge2_update_validator_tests().await; 84 | bridge2_withdrawal_tests().await; 85 | bridge2_locking_test().await; 86 | bridge2_batched_finalize_withdrawals_unit_tests().await; 87 | } 88 | 89 | async fn bridge2_end_to_end_test(http_client: &Arc, replicator: &Arc) { 90 | use crate::bridge_watcher2::DepositEvent; 91 | 92 | let chain = Chain::Local; 93 | let eth_chain = chain.eth_chain(); 94 | let owner_eth_client = chain.eth_client(Nickname::Owner).await; 95 | assert!(replicator.lock(|e| e.has_bridge2(), "integration_test_has_bridge2")); 96 | lu::async_sleep(Duration(1.)).await; 97 | let active_epoch = replicator.lock(|e| e.staking().active_epoch(), "integration_test_active_epoch"); 98 | assert_eq!( 99 | replicator.lock( 100 | |e| e.staking().validator_to_hot_user(tu::main_validator(), active_epoch).unwrap(), 101 | "integration_test_validator_to_hot_user" 102 | ), 103 | tu::main_validator_hot_user() 104 | ); 105 | 106 | let amount = 10.0; 107 | let amount_u64 = amount.with_decimals(USDC_ERC20_DECIMALS); 108 | 109 | let user = User::new(owner_eth_client.address()); 110 | let action = Box::new(OwnerMintTokenForAction { amount: 99, user }); 111 | let owner_wallet = Nickname::Owner.wallet(chain); 112 | let signed_action = SignedAction::new(action, &owner_wallet).unwrap(); 113 | http_client.send_signed_action(signed_action).await.unwrap().unwrap(); 114 | 115 | let action = Box::new(TokenDelegateAction { validator: tu::main_validator(), amount: 99 }); 116 | let signed_action = SignedAction::new(action, &owner_wallet).unwrap(); 117 | http_client.send_signed_action(signed_action).await.unwrap().unwrap(); 118 | 119 | let bridge2_cid = chain.bridge2_cid(); 120 | chain.usdc_cid().send("approve", (bridge2_cid.address(eth_chain), u64::MAX), &owner_eth_client).await.unwrap(); 121 | let tx_receipt = bridge2_cid.send("deposit", amount_u64, &owner_eth_client).await.unwrap(); 122 | TXS.lock().entry(bridge2_cid.address(eth_chain)).or_default().insert(tx_receipt.transaction_hash); 123 | 124 | lu::async_sleep(Duration(3.)).await; 125 | let deposit_events: Vec = owner_eth_client.parse_events(bridge2_cid, tx_receipt); 126 | let DepositEvent { usdc, .. } = *deposit_events.first().unwrap(); 127 | assert_eq!(usdc.to_string(), "10000000"); 128 | 129 | let action = Box::new(WithdrawAction { nonce: 0, usd: amount_u64 }); 130 | let signed_action = SignedAction::new(action, &owner_wallet).unwrap(); 131 | http_client.send_signed_action(signed_action).await.unwrap().unwrap(); 132 | 133 | lu::async_sleep(Duration(8.)).await; 134 | let claimable_withdrawals = 135 | replicator.lock(|e| e.claimable_withdrawals(user), "integration_test_claimable_withdrawals"); 136 | assert!(!claimable_withdrawals.is_empty()); 137 | let withdrawal_voucher = claimable_withdrawals.first().unwrap(); 138 | assert_eq!(withdrawal_voucher.signers, vec![tu::main_validator_hot_user()]); 139 | warn!("withdrawing from bridge2" => withdrawal_voucher, active_epoch); 140 | 141 | // NOSHIP write test for claiming and finalizing withdrawal successfully, too early, failure on multiple times 142 | } 143 | 144 | async fn l1_test(http_client: &Arc) { 145 | let chain = Chain::Local; 146 | let owner_wallet = Nickname::Owner.wallet(chain); 147 | let user_wallet = tu::wallet(1); 148 | let agent_wallet = tu::wallet(4); 149 | let req = utils::approve_agent_action(&user_wallet, &agent_wallet).unwrap(); 150 | 151 | http_client.send_signed_action(req).await.unwrap().unwrap(); 152 | 153 | http_client 154 | .send_signed_action( 155 | SignedAction::new( 156 | Box::new(RegisterAssetAction { 157 | coin: "ETH".to_string(), 158 | sz_decimals: 1, 159 | oracle_px: 1000., 160 | max_leverage: 50, 161 | }), 162 | &owner_wallet, 163 | ) 164 | .unwrap(), 165 | ) 166 | .await 167 | .unwrap() 168 | .unwrap(); 169 | http_client 170 | .send_signed_action( 171 | SignedAction::new( 172 | Box::new(RegisterAssetAction { 173 | coin: "BTC".to_string(), 174 | sz_decimals: 1, 175 | oracle_px: 1000., 176 | max_leverage: 50, 177 | }), 178 | &owner_wallet, 179 | ) 180 | .unwrap(), 181 | ) 182 | .await 183 | .unwrap() 184 | .unwrap(); 185 | 186 | let action = tu::o(1, Side::Bid, 123400000., 1.).into_action(); 187 | let req = SignedAction::new(action, &agent_wallet).unwrap(); 188 | let resp = http_client.send_signed_action(req).await.unwrap().unwrap(); 189 | let FSignedActionSuccess::Order(FOrderResponse { statuses }) = resp else { 190 | unreachable!("{resp:?}") 191 | }; 192 | 193 | assert_eq!(statuses.len(), 1); 194 | assert_eq!(statuses[0], FOrderStatus::Error("Insufficient margin to place order.".to_string())); 195 | 196 | let cancel_action = CancelAction { cancels: vec![Cancel { asset: 0, oid: 123 }] }; 197 | let req = SignedAction::new(Box::new(cancel_action), &agent_wallet).unwrap(); 198 | let resp = http_client.send_signed_action(req).await.unwrap().unwrap(); 199 | let FSignedActionSuccess::Cancel(FCancelResponse { statuses }) = resp else { 200 | unreachable!("{resp:?}") 201 | }; 202 | 203 | assert_eq!(statuses.len(), 1); 204 | assert!( 205 | u::serde_eq( 206 | &statuses[0], 207 | &FCancelStatus::Error("Order was never placed, already canceled, or filled.".to_string()) 208 | ), 209 | "{statuses:?}" 210 | ); 211 | } 212 | 213 | async fn bridge2_update_validator_tests() { 214 | // NOSHIP test validator set updates with cold keys different than hot keys 215 | let chain = Chain::Local; 216 | let eth_chain = chain.eth_chain(); 217 | let eth_client = tu::main_validator_eth_client().await; 218 | let wallet0 = tu::main_validator_hot_wallet(); 219 | let wallet1 = tu::wallet(1); 220 | let wallet2 = tu::wallet(2); 221 | 222 | let user0 = tu::main_validator_hot_user(); 223 | let user1 = wallet1.address().into(); 224 | let user2 = wallet2.address().into(); 225 | 226 | warn!("running bridge2_update_validator_tests" => user0, user1, user2, eth_client.address()); 227 | let cur_epoch: u64 = chain.bridge2_cid().call("epoch", (), ð_client).await.unwrap(); 228 | let new_epoch = cur_epoch + 1; 229 | let active_validator_set = initial_validator_set(); 230 | let new_validator_set = set![ 231 | ValidatorProfile { power: 50, hot_user: user0, cold_user: user0 }, 232 | ValidatorProfile { power: 50, hot_user: user1, cold_user: user1 } 233 | ]; 234 | sign_and_update_validator_set( 235 | ð_client, 236 | chain, 237 | &active_validator_set, 238 | &new_validator_set, 239 | cur_epoch, 240 | new_epoch, 241 | &[wallet0.clone()], 242 | ) 243 | .await 244 | .unwrap(); 245 | finalize_validator_set_update(ð_client, chain).await.unwrap(); 246 | 247 | let cur_epoch = new_epoch; 248 | let new_epoch = cur_epoch + 1; 249 | let active_validator_set = new_validator_set; 250 | let new_validator_set = set![ 251 | ValidatorProfile { power: 50, hot_user: user0, cold_user: user0 }, 252 | ValidatorProfile { power: 25, hot_user: user1, cold_user: user1 }, 253 | ValidatorProfile { power: 25, hot_user: user2, cold_user: user2 }, 254 | ]; 255 | sign_and_update_validator_set( 256 | ð_client, 257 | chain, 258 | &active_validator_set, 259 | &new_validator_set, 260 | cur_epoch, 261 | new_epoch, 262 | &[wallet0.clone(), wallet1.clone()], 263 | ) 264 | .await 265 | .unwrap(); 266 | finalize_validator_set_update(ð_client, chain).await.unwrap(); 267 | 268 | let amount = 10.0; 269 | let usd = amount.with_decimals(USDC_ERC20_DECIMALS); 270 | let nonce = 0; 271 | let hash = utils::keccak((user0.raw(), usd, nonce)); 272 | let signatures = vec![chain.sign_phantom_agent(hash, &wallet0)]; 273 | let withdrawal_voucher_no_quorum = WithdrawalVoucher2 { 274 | usd, 275 | nonce, 276 | active_validator_set: new_validator_set.clone(), 277 | signers: vec![user0], 278 | signatures, 279 | }; 280 | 281 | let res = tu::bridge2_withdrawal(&chain, ð_client, withdrawal_voucher_no_quorum, new_epoch).await; 282 | tu::assert_err(res, "Submitted validator set signatures do not have enough power"); 283 | 284 | chain.usdc_cid().send("approve", (chain.bridge2_cid().address(eth_chain), u64::MAX), ð_client).await.unwrap(); 285 | let owner_eth_client = chain.eth_client(Nickname::Owner).await; 286 | chain.usdc_cid().send("transfer", (user0.raw(), usd), &owner_eth_client).await.unwrap(); 287 | chain.bridge2_cid().send("deposit", usd, ð_client).await.unwrap(); 288 | 289 | let cur_epoch = new_epoch; 290 | let new_epoch = cur_epoch + 1; 291 | let sol_new_validator_set = SolValidatorSetUpdate::from_validator_set(new_epoch, &new_validator_set); 292 | let sol_active_validator_set = SolValidatorSet::from_hot_validator_set(cur_epoch, &active_validator_set); 293 | let sol_new_validator_set_hash = sol_new_validator_set.hash(); 294 | let mut signatures = Vec::new(); 295 | for validator in [&wallet0, &wallet1, &wallet2] { 296 | signatures.push(chain.sign_phantom_agent(sol_new_validator_set_hash, validator)); 297 | } 298 | let signers: Vec<_> = active_validator_set.iter().rev().map(|x| x.hot_user.raw()).collect(); 299 | let res = chain 300 | .bridge2_cid() 301 | .send( 302 | "updateValidatorSet", 303 | (sol_new_validator_set.clone(), sol_active_validator_set.clone(), signers, signatures.clone()), 304 | ð_client, 305 | ) 306 | .await; 307 | 308 | tu::assert_err(res, "Supplied active validators and powers do not match checkpoint"); 309 | 310 | let active_validator_set = set![ 311 | ValidatorProfile { power: 50, hot_user: user0, cold_user: user0 }, 312 | ValidatorProfile { power: 25, hot_user: user1, cold_user: user1 }, 313 | ValidatorProfile { power: 25, hot_user: user2, cold_user: user2 }, 314 | ]; 315 | let sol_new_validator_set = SolValidatorSetUpdate::from_validator_set(cur_epoch, &active_validator_set); 316 | let sol_active_validator_set = SolValidatorSet::from_hot_validator_set(cur_epoch, &active_validator_set); 317 | let res = chain 318 | .bridge2_cid() 319 | .send( 320 | "updateValidatorSet", 321 | ( 322 | sol_new_validator_set.clone(), 323 | sol_active_validator_set.clone(), 324 | sol_active_validator_set.validators.clone(), 325 | signatures.clone(), 326 | ), 327 | ð_client, 328 | ) 329 | .await; 330 | tu::assert_err(res, "New validator set epoch must be greater than the active epoch"); 331 | 332 | let mut wrong_signatures = Vec::new(); 333 | for validator in &[&wallet1, &wallet2, &wallet2] { 334 | wrong_signatures.push(chain.sign_phantom_agent(sol_new_validator_set.hash(), validator)); 335 | } 336 | let sol_new_validator_set = SolValidatorSetUpdate::from_validator_set(new_epoch, &active_validator_set); 337 | let res = chain 338 | .bridge2_cid() 339 | .send( 340 | "updateValidatorSet", 341 | ( 342 | sol_new_validator_set.clone(), 343 | sol_active_validator_set.clone(), 344 | sol_active_validator_set.validators.clone(), 345 | wrong_signatures, 346 | ), 347 | ð_client, 348 | ) 349 | .await; 350 | tu::assert_err(res, "Validator signature does not match"); 351 | 352 | let no_quorum_signatures = vec![chain.sign_phantom_agent(sol_new_validator_set.hash(), &wallet1)]; 353 | let res = chain 354 | .bridge2_cid() 355 | .send( 356 | "updateValidatorSet", 357 | (sol_new_validator_set, sol_active_validator_set.clone(), vec![user1.raw()], no_quorum_signatures), 358 | ð_client, 359 | ) 360 | .await; 361 | tu::assert_err(res, "Submitted validator set signatures do not have enough power"); 362 | 363 | let new_validator_wallets = utils::wallet_range(100); 364 | let new_validator_set = new_validator_wallets 365 | .iter() 366 | .enumerate() 367 | .map(|(u, wallet)| ValidatorProfile { 368 | power: (100 - u) as u64, 369 | hot_user: wallet.address().into(), 370 | cold_user: wallet.address().into(), 371 | }) 372 | .collect(); 373 | let active_validator_set = set![ 374 | ValidatorProfile { power: 50, hot_user: user0, cold_user: user0 }, 375 | ValidatorProfile { power: 25, hot_user: user1, cold_user: user1 }, 376 | ValidatorProfile { power: 25, hot_user: user2, cold_user: user2 }, 377 | ]; 378 | let wallets = [wallet0.clone(), wallet1.clone(), wallet2.clone()]; 379 | sign_and_update_validator_set( 380 | ð_client, 381 | chain, 382 | &active_validator_set, 383 | &new_validator_set, 384 | cur_epoch, 385 | new_epoch, 386 | &wallets, 387 | ) 388 | .await 389 | .unwrap(); 390 | finalize_validator_set_update(ð_client, chain).await.unwrap(); 391 | 392 | let cur_epoch = new_epoch; 393 | let new_epoch = cur_epoch + 1; 394 | let active_validator_set = new_validator_set; 395 | let new_validator_set = initial_validator_set(); 396 | sign_and_update_validator_set( 397 | ð_client, 398 | chain, 399 | &active_validator_set, 400 | &new_validator_set, 401 | cur_epoch, 402 | new_epoch, 403 | new_validator_wallets.as_slice(), 404 | ) 405 | .await 406 | .unwrap(); 407 | finalize_validator_set_update(ð_client, chain).await.unwrap(); 408 | } 409 | 410 | async fn bridge_end_to_end_test(http_client: &Arc, replicator: &Arc) { 411 | use crate::bridge_watcher::DepositEvent; 412 | warn!("starting bridge_end_to_end_test"); 413 | let chain = Chain::Local; 414 | let owner_eth_client = chain.eth_client(Nickname::Owner).await; 415 | let user_eth_client = chain.eth_client(Nickname::User).await; 416 | 417 | let initial_account_value = account_value(replicator, User::new(owner_eth_client.address())); 418 | let amount = 11.; 419 | let owner_usdc_amount = (5. * amount).with_decimals(USDC_ERC20_DECIMALS); 420 | let user_usdc_amount = (3. * amount).with_decimals(USDC_ERC20_DECIMALS); 421 | 422 | let bridge_cid = chain.bridge_cid(); 423 | chain.usdc_cid().send("mint", owner_usdc_amount + user_usdc_amount, &owner_eth_client).await.unwrap(); 424 | chain.usdc_cid().send("transfer", (user_eth_client.address(), user_usdc_amount), &owner_eth_client).await.unwrap(); 425 | chain 426 | .usdc_cid() 427 | .send("approve", (bridge_cid.address(owner_eth_client.chain()), owner_usdc_amount), &owner_eth_client) 428 | .await 429 | .unwrap(); 430 | chain 431 | .usdc_cid() 432 | .send("approve", (bridge_cid.address(owner_eth_client.chain()), user_usdc_amount), &user_eth_client) 433 | .await 434 | .unwrap(); 435 | 436 | let initial_owner_usdc_balance: u64 = 437 | chain.usdc_cid().call("balanceOf", owner_eth_client.address(), &owner_eth_client).await.unwrap(); 438 | assert_eq!(initial_owner_usdc_balance, owner_usdc_amount); 439 | let initial_not_owner_eth_client_usdc_balance: u64 = 440 | chain.usdc_cid().call("balanceOf", user_eth_client.address(), &owner_eth_client).await.unwrap(); 441 | assert_eq!(initial_not_owner_eth_client_usdc_balance, user_usdc_amount); 442 | let initial_user_usdc_balance: u64 = 443 | chain.usdc_cid().call("balanceOf", user_eth_client.address(), &owner_eth_client).await.unwrap(); 444 | assert_eq!(initial_user_usdc_balance, user_usdc_amount); 445 | 446 | let bridge_cid = chain.bridge_cid(); 447 | let tx_receipt = 448 | bridge_cid.send("deposit", amount.with_decimals(USDC_ERC20_DECIMALS), &owner_eth_client).await.unwrap(); 449 | let tx_hash = tx_receipt.clone().transaction_hash; 450 | TXS.lock().entry(bridge_cid.address(chain.eth_chain())).or_default().insert(tx_hash); 451 | 452 | let tx_receipt = 453 | bridge_cid.send("deposit", amount.with_decimals(USDC_ERC20_DECIMALS), &owner_eth_client).await.unwrap(); 454 | let tx_hash = tx_receipt.clone().transaction_hash; 455 | TXS.lock().entry(bridge_cid.address(chain.eth_chain())).or_default().insert(tx_hash); 456 | 457 | lu::async_sleep(Duration(5.)).await; 458 | 459 | let deposit_events: Vec = owner_eth_client.parse_events(bridge_cid, tx_receipt); 460 | let DepositEvent { user, usdc, .. } = *deposit_events.first().unwrap(); 461 | assert_eq!(usdc.to_string(), "11000000"); 462 | assert_eq!(user, owner_eth_client.address().raw()); 463 | let final_account_value = account_value(replicator, User::new(owner_eth_client.address())); 464 | 465 | warn!("should have deposited" => initial_account_value, final_account_value, owner_eth_client.address(), deposit_events); 466 | assert_eq!(final_account_value - initial_account_value, 2. * amount); 467 | let initial_account_value = account_value(replicator, User::new(owner_eth_client.address())); 468 | 469 | let nonce = 1; 470 | let withdraw_amount = 10.; 471 | let usd = withdraw_amount.with_decimals(USDC_ERC20_DECIMALS); 472 | let withdraw_action = WithdrawAction { usd, nonce }; 473 | 474 | let signing_key = SigningKey::from_bytes(&owner_eth_client.key().to_fixed_bytes()).unwrap(); 475 | let owner_wallet = Wallet::from(signing_key); 476 | let owner_agent = tu::wallet(5); 477 | let req = utils::approve_agent_action(&owner_wallet, &owner_agent).unwrap(); 478 | http_client.send_signed_action(req).await.unwrap().unwrap(); 479 | 480 | let req = SignedAction::new(Box::new(withdraw_action), &owner_agent).unwrap(); 481 | http_client.send_signed_action(req).await.unwrap().unwrap(); 482 | 483 | let owner = User::new(owner_eth_client.address()); 484 | let final_account_value = account_value(replicator, owner); 485 | assert_eq!(initial_account_value - final_account_value, withdraw_amount); 486 | 487 | let mut pending_withdrawals = replicator 488 | .lock(|e| e.bridge().pending_withdrawals(owner).unwrap().clone(), "integration_test_pending_withdrawals"); 489 | assert!(pending_withdrawals.len() == 1); 490 | let withdrawal_voucher = pending_withdrawals.pop().unwrap(); 491 | warn!("pending withdrawal" => withdrawal_voucher); 492 | 493 | let mut incorrect_amount_voucher = withdrawal_voucher.clone(); 494 | incorrect_amount_voucher.action.usd = 1000.0.with_decimals(USDC_ERC20_DECIMALS); 495 | let res = claim_withdrawal_on_bridge(chain, incorrect_amount_voucher, &owner_eth_client).await; 496 | assert!(res.err().unwrap().to_string().contains("Withdrawal not signed by Hyperliquid.")); 497 | 498 | let tx_receipt = claim_withdrawal_on_bridge(chain, withdrawal_voucher.clone(), &owner_eth_client).await.unwrap(); 499 | let tx_hash = tx_receipt.transaction_hash; 500 | TXS.lock().entry(bridge_cid.address(chain.eth_chain())).or_default().insert(tx_hash); 501 | 502 | let res = claim_withdrawal_on_bridge(chain, withdrawal_voucher, &owner_eth_client).await; 503 | assert!(res.err().unwrap().to_string().contains("Already withdrawn.")); 504 | 505 | lu::async_sleep(Duration(2.)).await; 506 | 507 | let pending_withdrawals = replicator 508 | .lock(|e| e.bridge().pending_withdrawals(owner).unwrap().clone(), "integration_test_pending_withdrawals"); 509 | warn!("should have no pending withdrawals" => owner, pending_withdrawals); 510 | 511 | let is_locked: bool = bridge_cid.call("isLocked", (), &owner_eth_client).await.unwrap(); 512 | assert!(!is_locked); 513 | let res = bridge_cid.send("setIsLocked", true, &user_eth_client).await; 514 | tu::assert_err(res, "Ownable: caller is not the owner"); 515 | bridge_cid.send("setIsLocked", true, &owner_eth_client).await.unwrap(); 516 | let is_locked: bool = bridge_cid.call("isLocked", (), &owner_eth_client).await.unwrap(); 517 | assert!(is_locked); 518 | 519 | let nonce = 2; 520 | let usd = 1.0.with_decimals(USDC_ERC20_DECIMALS); 521 | let withdraw_action = WithdrawAction { usd, nonce }; 522 | let req = SignedAction::new(Box::new(withdraw_action), &owner_agent).unwrap(); 523 | http_client.send_signed_action(req).await.unwrap().unwrap(); 524 | let mut pending_withdrawals = replicator 525 | .lock(|e| e.bridge().pending_withdrawals(owner).unwrap().clone(), "integration_test_pending_withdrawals"); 526 | let withdrawal_voucher = pending_withdrawals.pop().unwrap(); 527 | let res = claim_withdrawal_on_bridge(chain, withdrawal_voucher, &owner_eth_client).await; 528 | tu::assert_err(res, "Cannot withdraw/deposit from/to locked bridge"); 529 | 530 | let res = bridge_cid.send("withdrawAllUsdcToOwner", (), &user_eth_client).await; 531 | tu::assert_err(res, "Ownable: caller is not the owner"); 532 | let bridge_usdc_balance: u64 = 533 | chain.usdc_cid().call("balanceOf", bridge_cid.address(EthChain::Localhost), &owner_eth_client).await.unwrap(); 534 | let old_owner_usdc_balance: u64 = 535 | chain.usdc_cid().call("balanceOf", owner_eth_client.address(), &owner_eth_client).await.unwrap(); 536 | bridge_cid.send("withdrawAllUsdcToOwner", (), &owner_eth_client).await.unwrap(); 537 | let new_owner_usdc_balance: u64 = 538 | chain.usdc_cid().call("balanceOf", owner_eth_client.address(), &owner_eth_client).await.unwrap(); 539 | assert_eq!(new_owner_usdc_balance, old_owner_usdc_balance + bridge_usdc_balance); 540 | 541 | assert!(pending_withdrawals.is_empty()); 542 | 543 | bridge_cid.send("setIsLocked", false, &owner_eth_client).await.unwrap(); 544 | bridge_cid.send("setWhitelistOnly", true, &owner_eth_client).await.unwrap(); 545 | let res = bridge_cid.send("deposit", amount.with_decimals(USDC_ERC20_DECIMALS), &user_eth_client).await; 546 | tu::assert_err(res, "Sender is not whitelisted"); 547 | bridge_cid 548 | .send("setUsersWhitelistState", (vec![user_eth_client.address().raw()], true), &owner_eth_client) 549 | .await 550 | .unwrap(); 551 | bridge_cid.send("deposit", amount.with_decimals(USDC_ERC20_DECIMALS), &user_eth_client).await.unwrap(); 552 | 553 | bridge_cid 554 | .send("setMaxDepositPerPeriod", amount.with_decimals(USDC_ERC20_DECIMALS), &owner_eth_client) 555 | .await 556 | .unwrap(); 557 | let seconds_per_deposit_period: u64 = 24 * 60 * 60; 558 | bridge_cid.send("setSecondsPerDepositPeriod", seconds_per_deposit_period, &owner_eth_client).await.unwrap(); 559 | let receipt = 560 | bridge_cid.send("deposit", amount.with_decimals(USDC_ERC20_DECIMALS), &user_eth_client).await.unwrap(); 561 | let timestamp = user_eth_client.receipt_to_block(&receipt).await.unwrap().timestamp.without_decimals(0); 562 | 563 | let amount_deposited_this_period: u64 = bridge_cid 564 | .call( 565 | "amountDepositedPerPeriod", 566 | (timestamp as u64 / seconds_per_deposit_period, user_eth_client.address().raw()), 567 | &user_eth_client, 568 | ) 569 | .await 570 | .unwrap(); 571 | assert_eq!(amount_deposited_this_period, amount.with_decimals(USDC_ERC20_DECIMALS)); 572 | let res = bridge_cid.send("deposit", (0.5 * amount).with_decimals(USDC_ERC20_DECIMALS), &user_eth_client).await; 573 | tu::assert_err(res, "deposit amount exceeds the maximum for this period"); 574 | 575 | let usd = account_value(replicator, owner).with_decimals(USDC_ERC20_DECIMALS); 576 | let withdraw_action = WithdrawAction { usd, nonce }; 577 | let signed_action = SignedAction::new(Box::new(withdraw_action), &owner_agent).unwrap(); 578 | http_client.send_signed_action(signed_action).await.unwrap().unwrap(); 579 | assert_eq!(replicator.lock(|e| e.bridge().bal, "integration_test_bridge_bal"), 0); 580 | 581 | TXS.lock().clear(); 582 | } 583 | 584 | async fn bridge2_withdrawal_tests() { 585 | let chain = Chain::Local; 586 | let eth_client = tu::main_validator_eth_client().await; 587 | let hot_user = tu::main_validator_hot_user(); 588 | let hot_wallet = tu::main_validator_hot_wallet(); 589 | 590 | let powers = vec![55, 20, 10, 10, 6, 5, 3, 2, 1, 1]; 591 | let wallets = utils::wallet_range(10); 592 | let tot_power: u64 = powers.iter().sum(); 593 | 594 | let active_validator_set = initial_validator_set(); 595 | let cur_epoch: u64 = chain.bridge2_cid().call("epoch", (), ð_client).await.unwrap(); 596 | let new_epoch = cur_epoch + 1; 597 | let new_validator_set: Set<_> = wallets 598 | .iter() 599 | .enumerate() 600 | .map(|(i, wallet)| ValidatorProfile { 601 | power: powers[i], 602 | hot_user: wallet.address().into(), 603 | cold_user: tu::user((1000 + i) as u8), 604 | }) 605 | .collect(); 606 | 607 | sign_and_update_validator_set( 608 | ð_client, 609 | chain, 610 | &active_validator_set, 611 | &new_validator_set, 612 | cur_epoch, 613 | new_epoch, 614 | &[hot_wallet], 615 | ) 616 | .await 617 | .unwrap(); 618 | finalize_validator_set_update(ð_client, chain).await.unwrap(); 619 | let cur_epoch = new_epoch; 620 | 621 | let mut nonce = 0; 622 | let usd = 1.0.with_decimals(USDC_ERC20_DECIMALS); 623 | let owner_eth_client = chain.eth_client(Nickname::Owner).await; 624 | chain.usdc_cid().send("transfer", (hot_user.raw(), 30 * usd), &owner_eth_client).await.unwrap(); 625 | chain.bridge2_cid().send("deposit", 30 * usd, ð_client).await.unwrap(); 626 | 627 | let active_validator_set = new_validator_set; 628 | for inds in [ 629 | Vec::new(), 630 | vec![0], 631 | vec![3], 632 | vec![9], 633 | vec![1, 2, 3, 4, 5, 6], 634 | vec![0, 3, 6, 8], 635 | vec![6, 7], 636 | vec![0, 4, 8, 9], 637 | vec![0, 1, 2, 5, 6, 9], 638 | vec![1, 2, 6], 639 | vec![2, 3, 6, 7, 9], 640 | vec![0, 1, 4, 5, 9], 641 | vec![0, 5, 7, 8], 642 | vec![0, 1, 2, 3, 5, 7, 8], 643 | vec![1, 3, 6, 9], 644 | vec![1, 2, 4, 5, 7, 8, 9], 645 | vec![4, 6, 8], 646 | vec![0, 1, 2, 4, 5, 6, 9], 647 | vec![1, 2, 3, 7, 8, 9], 648 | vec![1, 4, 5, 6, 7, 8, 9], 649 | vec![0, 2, 3, 4, 5, 6, 7, 8, 9], 650 | vec![0, 2, 3, 7, 9], 651 | vec![2, 4, 5, 6, 7, 8], 652 | vec![0, 1, 3, 5, 6, 7, 9], 653 | vec![0, 5, 7, 8, 9], 654 | vec![0, 2, 4, 5, 7, 9], 655 | vec![0, 2, 5], 656 | vec![0, 1, 3, 4, 6, 7, 9], 657 | vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 658 | ] { 659 | let mut tot_sample_power = 0; 660 | let mut signatures = Vec::new(); 661 | let mut signers = Vec::new(); 662 | let mut sample_powers = Vec::new(); 663 | let hash = utils::keccak((hot_user.raw(), usd, nonce)); 664 | 665 | for &i in &inds { 666 | let wallet = &wallets[i]; 667 | let power = powers[i]; 668 | signatures.push(chain.sign_phantom_agent(hash, wallet)); 669 | signers.push(wallet.address().into()); 670 | tot_sample_power += power; 671 | sample_powers.push(power); 672 | } 673 | 674 | let quorum_reached = 3 * tot_sample_power >= 2 * tot_power; 675 | warn!("withdrawal fuzz test working on" => inds, quorum_reached); 676 | 677 | let withdrawal_voucher = 678 | WithdrawalVoucher2 { usd, nonce, signers, active_validator_set: active_validator_set.clone(), signatures }; 679 | nonce += 1; 680 | 681 | let res = tu::bridge2_withdrawal(&chain, ð_client, withdrawal_voucher, cur_epoch).await; 682 | assert_eq!(res.is_ok(), quorum_reached, "inds={inds:?} res={res:?}"); 683 | } 684 | 685 | // return to state that the node integration test can use 686 | let new_epoch = cur_epoch + 1; 687 | let new_validator_set = initial_validator_set(); 688 | sign_and_update_validator_set( 689 | ð_client, 690 | chain, 691 | &active_validator_set, 692 | &new_validator_set, 693 | cur_epoch, 694 | new_epoch, 695 | wallets.as_slice(), 696 | ) 697 | .await 698 | .unwrap(); 699 | finalize_validator_set_update(ð_client, chain).await.unwrap(); 700 | } 701 | 702 | async fn bridge2_locking_test() { 703 | let chain = Chain::Local; 704 | 705 | let eth_client = tu::main_validator_eth_client().await; 706 | let hot_user = tu::main_validator_hot_user(); 707 | let hot_wallet = tu::main_validator_hot_wallet(); 708 | let cold_wallet = tu::main_validator_cold_wallet(); 709 | let cold_user = tu::main_validator_cold_user(); 710 | 711 | let cur_epoch: u64 = chain.bridge2_cid().call("epoch", (), ð_client).await.unwrap(); 712 | let active_validator_set = initial_validator_set(); 713 | 714 | let sol_active_hot_valdiator_set = SolValidatorSet::from_hot_validator_set(cur_epoch, &active_validator_set); 715 | let unauthorized_locker = chain.eth_client(Nickname::User).await; 716 | let res = chain.bridge2_cid().send("emergencyLock", (), &unauthorized_locker).await; 717 | tu::assert_err(res, "Sender is not authorized to lock smart contract"); 718 | 719 | let locker = hot_user.raw(); 720 | let is_locker = true; 721 | let nonce = U256::zero(); 722 | let hash = utils::keccak(("modifyLocker".to_string(), locker, is_locker, nonce)); 723 | let signer = hot_user.raw(); 724 | let signature = chain.sign_phantom_agent(hash, &hot_wallet); 725 | chain 726 | .bridge2_cid() 727 | .send( 728 | "modifyLocker", 729 | (locker, is_locker, nonce, sol_active_hot_valdiator_set.clone(), vec![signer], vec![signature]), 730 | ð_client, 731 | ) 732 | .await 733 | .unwrap(); 734 | let res: bool = chain.bridge2_cid().call("isLocker", locker, ð_client).await.unwrap(); 735 | assert_eq!(res, is_locker); 736 | 737 | let user1 = tu::user(1); 738 | let finalizer = user1.raw(); 739 | let is_finalizer = true; 740 | let nonce = U256::zero(); 741 | let hash = utils::keccak(("modifyFinalizer".to_string(), finalizer, is_finalizer, nonce)); 742 | let signer = hot_user.raw(); 743 | let signature = chain.sign_phantom_agent(hash, &hot_wallet); 744 | chain 745 | .bridge2_cid() 746 | .send( 747 | "modifyFinalizer", 748 | (finalizer, is_finalizer, nonce, sol_active_hot_valdiator_set.clone(), vec![signer], vec![signature]), 749 | ð_client, 750 | ) 751 | .await 752 | .unwrap(); 753 | let res: bool = chain.bridge2_cid().call("isFinalizer", finalizer, ð_client).await.unwrap(); 754 | assert_eq!(res, is_finalizer); 755 | 756 | chain.bridge2_cid().send("emergencyLock", (), ð_client).await.unwrap(); 757 | 758 | let signer = cold_user.raw(); 759 | let new_dispute_period_seconds = U256::from(10); 760 | let nonce = U256::zero(); 761 | let sol_active_cold_validator_set = SolValidatorSet::from_cold_validator_set(cur_epoch, &active_validator_set); 762 | let hash = utils::keccak(("changeDisputePeriodSeconds".to_string(), new_dispute_period_seconds, nonce)); 763 | let signature = chain.sign_phantom_agent(hash, &cold_wallet); 764 | chain 765 | .bridge2_cid() 766 | .send( 767 | "changeDisputePeriodSeconds", 768 | (new_dispute_period_seconds, nonce, sol_active_cold_validator_set.clone(), vec![signer], vec![signature]), 769 | ð_client, 770 | ) 771 | .await 772 | .unwrap(); 773 | 774 | let res: U256 = chain.bridge2_cid().call("disputePeriodSeconds", (), ð_client).await.unwrap(); 775 | assert_eq!(res, new_dispute_period_seconds); 776 | 777 | let new_block_duration_millis = U256::from(200); 778 | let hash = utils::keccak(("changeBlockDurationMillis".to_string(), new_block_duration_millis, nonce)); 779 | let signature = chain.sign_phantom_agent(hash, &cold_wallet); 780 | chain 781 | .bridge2_cid() 782 | .send( 783 | "changeBlockDurationMillis", 784 | (new_block_duration_millis, nonce, sol_active_cold_validator_set.clone(), vec![signer], vec![signature]), 785 | ð_client, 786 | ) 787 | .await 788 | .unwrap(); 789 | 790 | let res: U256 = chain.bridge2_cid().call("blockDurationMillis", (), ð_client).await.unwrap(); 791 | assert_eq!(res, new_block_duration_millis); 792 | 793 | let new_epoch = cur_epoch + 1; 794 | let new_validator_set = initial_validator_set(); 795 | let sol_new_validator_set = SolValidatorSetUpdate::from_validator_set(new_epoch, &new_validator_set); 796 | let nonce = U256::zero(); 797 | let hash = utils::keccak(( 798 | "unlock".to_string(), 799 | sol_new_validator_set.epoch, 800 | sol_new_validator_set.clone().hot_addresses, 801 | sol_new_validator_set.clone().cold_addresses, 802 | sol_new_validator_set.clone().powers, 803 | nonce, 804 | )); 805 | let signature = chain.sign_phantom_agent(hash, &cold_wallet); 806 | chain 807 | .bridge2_cid() 808 | .send( 809 | "emergencyUnlock", 810 | (sol_new_validator_set, sol_active_cold_validator_set, vec![signer], vec![signature], nonce), 811 | ð_client, 812 | ) 813 | .await 814 | .unwrap(); 815 | 816 | let cur_epoch = new_epoch; 817 | let active_validator_set = new_validator_set; 818 | let sol_active_cold_valdiator_set = SolValidatorSet::from_cold_validator_set(cur_epoch, &active_validator_set); 819 | let locker = hot_user.raw(); 820 | let is_locker = false; 821 | let nonce = U256::one(); 822 | let hash = utils::keccak(("modifyLocker".to_string(), locker, is_locker, nonce)); 823 | let signer = cold_user.raw(); 824 | let signature = chain.sign_phantom_agent(hash, &cold_wallet); 825 | chain 826 | .bridge2_cid() 827 | .send( 828 | "modifyLocker", 829 | (locker, is_locker, nonce, sol_active_cold_valdiator_set.clone(), vec![signer], vec![signature]), 830 | ð_client, 831 | ) 832 | .await 833 | .unwrap(); 834 | let res: bool = chain.bridge2_cid().call("isLocker", locker, ð_client).await.unwrap(); 835 | assert_eq!(res, is_locker); 836 | 837 | let finalizer = user1.raw(); 838 | let is_finalizer = false; 839 | let nonce = U256::one(); 840 | let hash = utils::keccak(("modifyFinalizer".to_string(), finalizer, is_finalizer, nonce)); 841 | let signer = cold_user.raw(); 842 | let signature = chain.sign_phantom_agent(hash, &cold_wallet); 843 | chain 844 | .bridge2_cid() 845 | .send( 846 | "modifyFinalizer", 847 | (finalizer, is_finalizer, nonce, sol_active_cold_valdiator_set, vec![signer], vec![signature]), 848 | ð_client, 849 | ) 850 | .await 851 | .unwrap(); 852 | let res: bool = chain.bridge2_cid().call("isFinalizer", finalizer, ð_client).await.unwrap(); 853 | assert_eq!(res, is_finalizer); 854 | } 855 | 856 | async fn bridge2_batched_finalize_withdrawals_unit_tests() { 857 | let chain = Chain::Local; 858 | let eth_client = tu::main_validator_eth_client().await; 859 | let hot_user = tu::main_validator_hot_user(); 860 | let hot_wallet = tu::main_validator_hot_wallet(); 861 | let cold_wallet = tu::main_validator_cold_wallet(); 862 | let cur_epoch: u64 = chain.bridge2_cid().call("epoch", (), ð_client).await.unwrap(); 863 | let active_validator_set = initial_validator_set(); 864 | 865 | let usd = 10.0.with_decimals(USDC_ERC20_DECIMALS); 866 | // Approve usdc and initialize bridge with sufficient funds 867 | let owner_eth_client = chain.eth_client(Nickname::Owner).await; 868 | chain 869 | .usdc_cid() 870 | .send("approve", (chain.bridge2_cid().address(chain.eth_chain()), u64::MAX), &owner_eth_client) 871 | .await 872 | .unwrap(); 873 | chain 874 | .usdc_cid() 875 | .send("approve", (chain.bridge2_cid().address(chain.eth_chain()), u64::MAX), ð_client) 876 | .await 877 | .unwrap(); 878 | let initial_bridge_bal: U256 = 879 | chain.usdc_cid().call("balanceOf", chain.bridge2_cid().address(chain.eth_chain()), ð_client).await.unwrap(); 880 | let initial_user_bal: U256 = chain.usdc_cid().call("balanceOf", hot_user.to_address(), ð_client).await.unwrap(); 881 | let to_transfer = 30 * usd - initial_bridge_bal.as_u64(); 882 | let to_mint = to_transfer - initial_user_bal.as_u64(); 883 | chain.usdc_cid().send("mint", to_mint, &owner_eth_client).await.unwrap(); 884 | chain.usdc_cid().send("transfer", (hot_user.raw(), to_mint), &owner_eth_client).await.unwrap(); 885 | chain.bridge2_cid().send("deposit", to_transfer, ð_client).await.unwrap(); 886 | let bridge_bal: U256 = 887 | chain.usdc_cid().call("balanceOf", chain.bridge2_cid().address(chain.eth_chain()), ð_client).await.unwrap(); 888 | assert_eq!(bridge_bal, U256::from(30 * usd)); 889 | let user_bal: U256 = chain.usdc_cid().call("balanceOf", hot_user.to_address(), ð_client).await.unwrap(); 890 | assert_eq!(user_bal, U256::zero()); 891 | 892 | // Set dispute period and block duration so that we can test that dispute period works. 893 | // The bridge must be locked to set the dispute period and block duration, which requires a locker. 894 | let locker = hot_user.raw(); 895 | let is_locker = true; 896 | let nonce = U256::from(2); 897 | let hash = utils::keccak(("modifyLocker".to_string(), locker, is_locker, nonce)); 898 | let signature = bridge2_sign_phantom_agent(chain, hash, &hot_wallet); 899 | chain 900 | .bridge2_cid() 901 | .send( 902 | "modifyLocker", 903 | ( 904 | locker, 905 | is_locker, 906 | nonce, 907 | SolValidatorSet::from_hot_validator_set(cur_epoch, &active_validator_set), 908 | vec![signature], 909 | ), 910 | ð_client, 911 | ) 912 | .await 913 | .unwrap(); 914 | 915 | chain.bridge2_cid().send("emergencyLock", (), ð_client).await.unwrap(); 916 | 917 | let sol_active_cold_validator_set = SolValidatorSet::from_cold_validator_set(cur_epoch, &active_validator_set); 918 | perform_bridge_2_validator_action( 919 | "changeDisputePeriodSeconds", 920 | U256::from(1), 921 | nonce, 922 | chain, 923 | &cold_wallet, 924 | &sol_active_cold_validator_set, 925 | ð_client, 926 | ) 927 | .await; 928 | perform_bridge_2_validator_action( 929 | "changeBlockDurationMillis", 930 | U256::from(1000), 931 | nonce, 932 | chain, 933 | &cold_wallet, 934 | &sol_active_cold_validator_set, 935 | ð_client, 936 | ) 937 | .await; 938 | 939 | let cur_epoch = cur_epoch + 1; 940 | let sol_new_validator_set = SolValidatorSetUpdate::from_validator_set(cur_epoch, &active_validator_set); 941 | let hash = utils::keccak(( 942 | "unlock".to_string(), 943 | sol_new_validator_set.epoch, 944 | sol_new_validator_set.clone().hot_addresses, 945 | sol_new_validator_set.clone().cold_addresses, 946 | sol_new_validator_set.clone().powers, 947 | nonce, 948 | )); 949 | let signature = bridge2_sign_phantom_agent(chain, hash, &cold_wallet); 950 | chain 951 | .bridge2_cid() 952 | .send( 953 | "emergencyUnlock", 954 | (sol_new_validator_set, sol_active_cold_validator_set, vec![signature], nonce), 955 | ð_client, 956 | ) 957 | .await 958 | .unwrap(); 959 | 960 | // Request withdrawal should work and produce receipt 961 | let nonce = 11; 962 | let hash = withdrawal_hash(hot_user, usd, nonce); 963 | let signatures = vec![bridge2_sign_phantom_agent(chain, hash, &hot_wallet)]; 964 | let withdrawal_voucher = 965 | WithdrawalVoucher2 { usd, nonce, active_validator_set: active_validator_set.clone(), signatures }; 966 | let tx_receipt = tu::bridge2_withdrawal(&chain, ð_client, withdrawal_voucher, cur_epoch).await.unwrap(); 967 | let requested_withdrawals: Vec = eth_client.parse_events(chain.bridge2_cid(), tx_receipt); 968 | assert_eq!(requested_withdrawals.len(), 1); 969 | let bridge_bal: U256 = 970 | chain.usdc_cid().call("balanceOf", chain.bridge2_cid().address(chain.eth_chain()), ð_client).await.unwrap(); 971 | assert_eq!(bridge_bal, U256::from(30 * usd)); 972 | let user_bal: U256 = chain.usdc_cid().call("balanceOf", hot_user.to_address(), ð_client).await.unwrap(); 973 | assert_eq!(user_bal, U256::zero()); 974 | 975 | // Finalizing withdrawal should fail due to dispute period 976 | let message = requested_withdrawals.first().unwrap().message; 977 | let result = chain.bridge2_cid().send("batchedFinalizeWithdrawals", vec![message], ð_client).await; 978 | tu::assert_err(result, "Still in dispute period"); 979 | let bridge_bal: U256 = 980 | chain.usdc_cid().call("balanceOf", chain.bridge2_cid().address(chain.eth_chain()), ð_client).await.unwrap(); 981 | assert_eq!(bridge_bal, U256::from(30 * usd)); 982 | let user_bal: U256 = chain.usdc_cid().call("balanceOf", hot_user.to_address(), ð_client).await.unwrap(); 983 | assert_eq!(user_bal, U256::zero()); 984 | 985 | // Sleep for longer than dispute period and try again should succeed 986 | lu::async_sleep(Duration(1.)).await; 987 | let tx_receipt = chain.bridge2_cid().send("batchedFinalizeWithdrawals", vec![message], ð_client).await.unwrap(); 988 | let withdrawal_finalization_events: Vec = 989 | eth_client.parse_events(chain.bridge2_cid(), tx_receipt); 990 | assert_eq!(withdrawal_finalization_events.len(), 1); 991 | let bridge_bal: U256 = 992 | chain.usdc_cid().call("balanceOf", chain.bridge2_cid().address(chain.eth_chain()), ð_client).await.unwrap(); 993 | assert_eq!(bridge_bal, U256::from(29 * usd)); 994 | let user_bal: U256 = chain.usdc_cid().call("balanceOf", hot_user.to_address(), ð_client).await.unwrap(); 995 | assert_eq!(user_bal, U256::from(usd)); 996 | 997 | // Try to finalize the same receipt should fail 998 | let result = chain.bridge2_cid().send("batchedFinalizeWithdrawals", vec![message], ð_client).await; 999 | tu::assert_err(result, "Withdrawal already finalized"); 1000 | let bridge_bal: U256 = 1001 | chain.usdc_cid().call("balanceOf", chain.bridge2_cid().address(chain.eth_chain()), ð_client).await.unwrap(); 1002 | assert_eq!(bridge_bal, U256::from(29 * usd)); 1003 | let user_bal: U256 = chain.usdc_cid().call("balanceOf", hot_user.to_address(), ð_client).await.unwrap(); 1004 | assert_eq!(user_bal, U256::from(usd)); 1005 | } 1006 | 1007 | fn initial_validator_set() -> Set { 1008 | set![ValidatorProfile { 1009 | power: 1, 1010 | hot_user: tu::main_validator_hot_user(), 1011 | cold_user: tu::main_validator_cold_user(), 1012 | }] 1013 | } 1014 | 1015 | async fn setup() -> Recver { 1016 | assert!(!*NODES_RUNNING.lock()); 1017 | *NODES_RUNNING.lock() = true; 1018 | TendermintInit::Wipe.run(None, TendermintHome::Normal); 1019 | let (sender, recver) = channel(); 1020 | let hyper_abci = HyperAbci::new(AbciStateBuilder::New { chain: Chain::Local }, sender, false, None); 1021 | std::thread::spawn(move || hyper_abci.run_server_blocking(TendermintHome::Normal)); 1022 | spawn_hardhat_node().await; 1023 | lu::async_sleep(Duration(1.)).await; 1024 | recver 1025 | } 1026 | 1027 | async fn setup_web_server(db_hub: Arc, replicator: Arc) { 1028 | let localhost = Ipv4Addr::LOCALHOST; 1029 | let (block_events_sender, block_events_recver) = channel(); 1030 | let web_server_config = WebServerConfig { 1031 | n_nodes: Some(1), 1032 | node_ip: localhost, 1033 | internal_ip: localhost, 1034 | print_requests: false, 1035 | chain: Chain::Local, 1036 | db_hub, 1037 | replicator, 1038 | block_events_sender, 1039 | block_events_recver, 1040 | web_server_port: 3002, 1041 | should_run_solo_watcher: true, 1042 | db_init: DbInit::Legacy, 1043 | finished_seeding_explorer: Arc::new(AtomicBool::new(true)), 1044 | }; 1045 | web_server_config.run().await; 1046 | } 1047 | 1048 | async fn spawn_hardhat_node() { 1049 | std::thread::spawn(|| { 1050 | shell("(cd ~/cham/code/hyperliquid && npx hardhat node) > /tmp/hardhat_out 2>&1".to_string()).wait_check() 1051 | }); 1052 | lu::async_sleep(Duration(1.)).await; 1053 | shell("(cd ~/cham/code/hyperliquid && npx hardhat run deploy/contracts.js --network localhost) > /tmp/hardhat_deploy_out 2>&1".to_string()).wait_check(); 1054 | } 1055 | 1056 | fn account_value(replicator: &Replicator, user: User) -> f64 { 1057 | replicator 1058 | .lock(|e| e.web_data(Some(user)), "integration_test_account_value") 1059 | .user_state 1060 | .margin_summary 1061 | .account_value 1062 | .0 1063 | } 1064 | 1065 | async fn claim_withdrawal_on_bridge( 1066 | chain: Chain, 1067 | withdrawal_voucher: WithdrawalVoucher, 1068 | client: &EthClient, 1069 | ) -> infra::Result { 1070 | let WithdrawalVoucher { signature, action: WithdrawAction { usd, nonce }, .. } = withdrawal_voucher; 1071 | chain.bridge_cid().send("withdraw", (signature, usd, nonce), client).await 1072 | } 1073 | 1074 | async fn perform_bridge_2_validator_action( 1075 | action: &str, 1076 | arg: T, 1077 | nonce: U256, 1078 | chain: Chain, 1079 | wallet: &Wallet, 1080 | sol_validator_set: &SolValidatorSet, 1081 | eth_client: &EthClient, 1082 | ) { 1083 | let hash = utils::keccak((action.to_string(), arg, nonce)); 1084 | let signature = bridge2_sign_phantom_agent(chain, hash, wallet); 1085 | chain 1086 | .bridge2_cid() 1087 | .send(action, (arg, nonce, sol_validator_set.clone(), vec![signature]), eth_client) 1088 | .await 1089 | .unwrap(); 1090 | } 1091 | 1092 | lazy_static! { 1093 | static ref NODES_RUNNING: Mutex = Mutex::new(false); 1094 | } 1095 | --------------------------------------------------------------------------------