From bf65e630331bc7c401bcbb5080f7507715ee2332 Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 3 Oct 2024 00:34:45 +0200 Subject: [PATCH 1/6] script: Adds ETH rescue tooling for deactivated opScribes --- script/Scribe.s.sol | 4 +- script/ScribeOptimistic.s.sol | 81 ++++++++++++++++++++++- script/rescue/Rescuer.sol | 120 ++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 script/rescue/Rescuer.sol diff --git a/script/Scribe.s.sol b/script/Scribe.s.sol index 32977ac..f6651a8 100644 --- a/script/Scribe.s.sol +++ b/script/Scribe.s.sol @@ -189,10 +189,10 @@ contract ScribeScript is Script { /// pokes with an already fully constructed payload. /// /// @dev Call via: - /// /// ```bash /// $ forge script \ - /// --private-key $PRIVATE_KEY \ + /// --keystore $KEYSTORE \ + /// --password $KEYSTORE_PASSWORD \ /// --broadcast \ /// --rpc-url $RPC_URL \ /// --sig $(cast calldata "pokeRaw(address,bytes)" $SCRIBE $PAYLOAD) \ diff --git a/script/ScribeOptimistic.s.sol b/script/ScribeOptimistic.s.sol index dae6ca9..bf5a7b0 100644 --- a/script/ScribeOptimistic.s.sol +++ b/script/ScribeOptimistic.s.sol @@ -15,10 +15,18 @@ import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; import {ScribeScript} from "./Scribe.s.sol"; +import {LibRandom} from "./libs/LibRandom.sol"; +import {LibFeed} from "./libs/LibFeed.sol"; + +import {Rescuer} from "./rescue/Rescuer.sol"; + /** * @title ScribeOptimistic Management Script */ contract ScribeOptimisticScript is ScribeScript { + using LibSecp256k1 for LibSecp256k1.Point; + using LibFeed for LibFeed.Feed; + /// @dev Deploys a new ScribeOptimistic instance with `initialAuthed` being /// the address initially auth'ed. Note that zero address is kissed /// directly after deployment. @@ -65,10 +73,10 @@ contract ScribeOptimisticScript is ScribeScript { /// opPokes with an already fully constructed payload. /// /// @dev Call via: - /// /// ```bash /// $ forge script \ - /// --private-key $PRIVATE_KEY \ + /// --keystore $KEYSTORE \ + /// --password $KEYSTORE_PASSWORD \ /// --broadcast \ /// --rpc-url $RPC_URL \ /// --sig $(cast calldata "opPokeRaw(address,bytes)" $SCRIBE $PAYLOAD) \ @@ -108,4 +116,73 @@ contract ScribeOptimisticScript is ScribeScript { IScribeOptimistic(self).opPoke(pokeData, schnorrData, ecdsaData); vm.stopBroadcast(); } + + /// @dev Rescues ETH held in deactivated `self`. + /// + /// @dev Call via: + /// ```bash + /// $ forge script \ + /// --keystore $KEYSTORE \ + /// --password $KEYSTORE_PASSWORD \ + /// --broadcast \ + /// --rpc-url $RPC_URL \ + /// --sig $(cast calldata "rescueETH(address,address)" $SCRIBE $RESCUER) \ + /// -vvvvv \ + /// script/dev/ScribeOptimistic.s.sol:ScribeOptimisticScript + /// ``` + function rescueETH(address self, address rescuer) public { + // Require self to be deactivated. + { + vm.prank(address(0)); + (bool ok, /*val*/ ) = IScribe(self).tryRead(); + require(!ok, "Instance not deactivated: read() does not fail"); + + require( + IScribe(self).feeds().length == 0, + "Instance not deactivated: Feeds still lifted" + ); + require( + IScribe(self).bar() == 255, + "Instance not deactivated: Bar not type(uint8).max" + ); + } + + // Ensure challenge reward is total balance. + uint challengeReward = IScribeOptimistic(self).challengeReward(); + uint total = self.balance; + if (challengeReward < total) { + IScribeOptimistic(self).setMaxChallengeReward(type(uint).max); + } + + // Create new random private key. + uint privKeySeed = LibRandom.readUint(); + uint privKey = _bound(privKeySeed, 1, LibSecp256k1.Q() - 1); + + // Create feed instance from private key. + LibFeed.Feed memory feed = LibFeed.newFeed(privKey); + + // Let feed sign feed registration message. + IScribe.ECDSAData memory registrationSig; + registrationSig = + feed.signECDSA(IScribe(self).feedRegistrationMessage()); + + // Construct pokeData and invalid Schnorr signature. + uint32 pokeDataAge = uint32(block.timestamp); + IScribe.PokeData memory pokeData = IScribe.PokeData(0, pokeDataAge); + IScribe.SchnorrData memory schnorrData = + IScribe.SchnorrData(bytes32(0), address(0), hex""); + + // Construct opPokeMessage. + bytes32 opPokeMessage = IScribeOptimistic(self).constructOpPokeMessage( + pokeData, schnorrData + ); + + // Let feed sign opPokeMessage. + IScribe.ECDSAData memory opPokeSig = feed.signECDSA(opPokeMessage); + + // Rescue ETH via rescuer contract. + Rescuer(payable(rescuer)).suck( + self, feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + } } diff --git a/script/rescue/Rescuer.sol b/script/rescue/Rescuer.sol new file mode 100644 index 0000000..41fdff4 --- /dev/null +++ b/script/rescue/Rescuer.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {IAuth} from "chronicle-std/auth/IAuth.sol"; +import {Auth} from "chronicle-std/auth/Auth.sol"; + +import {IScribe} from "src/IScribe.sol"; +import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; + +import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; + +/** + * @title Rescuer + * + * @notice Contract to recover ETH from offboarded ScribeOptimistic instances + * + * @dev Deployment: + * ```bash + * $ forge create script/rescue/Rescuer.sol:Rescuer \ + * --constructor-args $INITIAL_AUTHED \ + * --keystore $KEYSTORE \ + * --password $KEYSTORE_PASSWORD \ + * --rpc-url $RPC_URL \ + * --verifier-url $ETHERSCAN_API_URL \ + * --etherscan-api-key $ETHERSCAN_API_KEY + * ``` + * + * @author Chronicle Labs, Inc + * @custom:security-contact security@chroniclelabs.org + */ +contract Rescuer is Auth { + using LibSecp256k1 for LibSecp256k1.Point; + + /// @notice Emitted when successfully recovered ETH funds. + /// @param caller The caller's address. + /// @param opScribe The ScribeOptimistic instance the ETH got recovered + /// from. + /// @param amount The amount of ETH recovered. + event Recovered( + address indexed caller, address indexed opScribe, uint amount + ); + + /// @notice Emitted when successfully withdrawed ETH from this contract. + /// @param caller The caller's address. + /// @param receiver The receiver + /// from. + /// @param amount The amount of ETH recovered. + event Withdrawed( + address indexed caller, address indexed receiver, uint amount + ); + + constructor(address initialAuthed) Auth(initialAuthed) {} + + receive() external payable {} + + /// @notice Withdraws `amount` ETH held in contract to `receiver`. + /// + /// @dev Only callable by auth'ed address. + function withdraw(address payable receiver, uint amount) external auth { + (bool ok,) = receiver.call{value: amount}(""); + require(ok); + + emit Withdrawed(msg.sender, receiver, amount); + } + + /// @notice Rescues ETH from ScribeOptimistic instance `opScribe`. + /// + /// @dev Note that `opScribe` MUST be deactivated. + /// @dev Note that validator key pair SHALL be only used once and generated + /// via a CSPRNG. + /// + /// @dev Only callable by auth'ed address. + function suck( + address opScribe, + LibSecp256k1.Point memory pubKey, + IScribe.ECDSAData memory registrationSig, + uint32 pokeDataAge, + IScribe.ECDSAData memory opPokeSig + ) external auth { + require(IAuth(opScribe).authed(address(this))); + + address validator = pubKey.toAddress(); + uint8 validatorId = uint8(uint(uint160(validator)) >> 152); + + uint balanceBefore = address(this).balance; + + // Fail if instance has feeds lifted, ie is not deactivated. + require(IScribe(opScribe).feeds().length == 0); + + // Construct pokeData. + IScribe.PokeData memory pokeData = + IScribe.PokeData({val: uint128(0), age: pokeDataAge}); + + // Construct invalid Schnorr signature. + IScribe.SchnorrData memory schnorrSig = IScribe.SchnorrData({ + signature: bytes32(0), + commitment: address(0), + feedIds: hex"" + }); + + // Lift validator. + IScribe(opScribe).lift(pubKey, registrationSig); + + // Perform opPoke. + IScribeOptimistic(opScribe).opPoke(pokeData, schnorrSig, opPokeSig); + + // Perform opChallenge. + bool ok = IScribeOptimistic(opScribe).opChallenge(schnorrSig); + require(ok); + + // Drop validator again. + IScribe(opScribe).drop(validatorId); + + // Compute amount of ETH received as challenge reward. + uint amount = address(this).balance - balanceBefore; + + // Emit event. + emit Recovered(msg.sender, opScribe, amount); + } +} From 027425d174572ea223cdc0dcfd7db8dfb12c9932 Mon Sep 17 00:00:00 2001 From: Max Wickham Date: Thu, 3 Oct 2024 12:15:31 +0100 Subject: [PATCH 2/6] tests --- script/rescue/Rescuer.sol | 25 ++++ test/rescue/RescuerTest.sol | 246 ++++++++++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 test/rescue/RescuerTest.sol diff --git a/script/rescue/Rescuer.sol b/script/rescue/Rescuer.sol index 41fdff4..6b445ea 100644 --- a/script/rescue/Rescuer.sol +++ b/script/rescue/Rescuer.sol @@ -77,6 +77,29 @@ contract Rescuer is Auth { uint32 pokeDataAge, IScribe.ECDSAData memory opPokeSig ) external auth { + _suck(opScribe, pubKey, registrationSig, pokeDataAge, opPokeSig); + } + + /// @notice Rescues ETH from multiple ScribeOptimistic instances `opScribes`. + function suck( + address[] memory opScribes, + LibSecp256k1.Point memory pubKey, + IScribe.ECDSAData memory registrationSig, + uint32 pokeDataAge, + IScribe.ECDSAData memory opPokeSig + ) external auth { + for (uint i = 0; i < opScribes.length; i++) { + _suck(opScribes[i], pubKey, registrationSig, pokeDataAge, opPokeSig); + } + } + + function _suck( + address opScribe, + LibSecp256k1.Point memory pubKey, + IScribe.ECDSAData memory registrationSig, + uint32 pokeDataAge, + IScribe.ECDSAData memory opPokeSig + ) internal { require(IAuth(opScribe).authed(address(this))); address validator = pubKey.toAddress(); @@ -117,4 +140,6 @@ contract Rescuer is Auth { // Emit event. emit Recovered(msg.sender, opScribe, amount); } + + } diff --git a/test/rescue/RescuerTest.sol b/test/rescue/RescuerTest.sol new file mode 100644 index 0000000..5b79619 --- /dev/null +++ b/test/rescue/RescuerTest.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {console2} from "forge-std/console2.sol"; +import {Test} from "forge-std/Test.sol"; + +import {IAuth} from "chronicle-std/auth/IAuth.sol"; + +import {Rescuer} from "script/rescue/Rescuer.sol"; + +import {IScribe} from "src/IScribe.sol"; +import {IScribeOptimistic} from "src/IScribeOptimistic.sol"; +import {ScribeOptimistic} from "src/ScribeOptimistic.sol"; + +import {LibSecp256k1} from "src/libs/LibSecp256k1.sol"; + +import {LibFeed} from "script/libs/LibFeed.sol"; + +contract RescuerTest is Test { + using LibSecp256k1 for LibSecp256k1.Point; + using LibFeed for LibFeed.Feed; + + event Recovered( + address indexed caller, address indexed opScribe, uint amount + ); + event Withdrawed( + address indexed caller, address indexed receiver, uint amount + ); + + IScribeOptimistic private scribe; + Rescuer private rescuer; + + function setUp() public { + scribe = new ScribeOptimistic(address(this), bytes32("TEST/TEST")); + IScribeOptimistic(scribe).setMaxChallengeReward(type(uint).max); + rescuer = new Rescuer(address(this)); + } + + function test_suck() public { + // Auth the recover contract on scribe + IAuth(address(scribe)).rely(address(rescuer)); + // Send some Eth to the scribe contract + vm.deal(address(scribe), 1 ether); + // Create a new feed + LibFeed.Feed memory feed = LibFeed.newFeed(1); + // Create registration sig + IScribe.ECDSAData memory registrationSig; + registrationSig = + feed.signECDSA(scribe.feedRegistrationMessage()); + // Construct opPokeSignature, (with invalid schnorr sig) + uint32 pokeDataAge = uint32(block.timestamp); + IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); + // Rescue ETH via rescuer contract. + vm.expectEmit(); + emit Recovered(address(this), address(scribe), 1 ether); + rescuer.suck( + address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + // Withdraw the eth + uint current_balance = address(this).balance; + uint withdraw_amount = address(rescuer).balance; + address recipient = address(0x1234567890123456789012345678901234567890); + vm.expectEmit(); + emit Withdrawed(address(this), recipient, withdraw_amount); + rescuer.withdraw(payable(recipient), withdraw_amount); + assertEq(recipient.balance, withdraw_amount); + + } + + function test_suckMultiple() public { + address[] memory scribes = new address[](10); + for (uint i = 0; i < 10; i++) { + scribes[i] = address(new ScribeOptimistic(address(this), bytes32("TEST/TEST"))); + IScribeOptimistic(scribes[i]).setMaxChallengeReward(type(uint).max); + // Auth the recover contract on scribe + IAuth(address(scribes[i])).rely(address(rescuer)); + vm.deal(address(scribes[i]), 1 ether); + } + // Create a new feed + LibFeed.Feed memory feed = LibFeed.newFeed(1); + // Create registration sig + IScribe.ECDSAData memory registrationSig; + registrationSig = + feed.signECDSA(scribe.feedRegistrationMessage()); + // Construct opPokeSignature, (with invalid schnorr sig) + uint32 pokeDataAge = uint32(block.timestamp); + IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); + // Rescue ETH via rescuer contract. + for (uint i = 0; i < 10; i++) { + vm.expectEmit(); + emit Recovered(address(this), address(scribes[i]), 1 ether); + } + rescuer.suck( + scribes, feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + // Withdraw the eth + uint current_balance = address(this).balance; + uint withdraw_amount = address(rescuer).balance; + address recipient = address(0x1234567890123456789012345678901234567890); + vm.expectEmit(); + emit Withdrawed(address(this), recipient, withdraw_amount); + rescuer.withdraw(payable(recipient), withdraw_amount); + assertEq(recipient.balance, withdraw_amount); + + } + + function test_suck_FailsIf_rescuerNotAuthed() public { + // Send some Eth to the scribe contract + vm.deal(address(scribe), 1 ether); + // Create a new feed + LibFeed.Feed memory feed = LibFeed.newFeed(1); + // Create registration sig + IScribe.ECDSAData memory registrationSig; + registrationSig = + feed.signECDSA(scribe.feedRegistrationMessage()); + // Construct opPokeSignature, (with invalid schnorr sig) + uint32 pokeDataAge = uint32(block.timestamp); + IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); + // Rescue ETH via rescuer contract. + vm.expectRevert(); + rescuer.suck( + address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + } + + function test_suck_FailsIf_feedsLengthNotZero() public { + // Create a new feed to lift on scribe + LibFeed.Feed memory existing_feed = LibFeed.newFeed(1); + IScribe.ECDSAData memory registrationSig; + registrationSig = + existing_feed.signECDSA(scribe.feedRegistrationMessage()); + // Lift validator. + scribe.lift(existing_feed.pubKey, registrationSig); + // Auth the recover contract on scribe + IAuth(address(scribe)).rely(address(rescuer)); + // Send some Eth to the scribe contract + vm.deal(address(scribe), 1 ether); + // Create a new feed + LibFeed.Feed memory feed = LibFeed.newFeed(2); + // Create registration sig + registrationSig = + feed.signECDSA(scribe.feedRegistrationMessage()); + // Construct opPokeSignature, (with invalid schnorr sig) + uint32 pokeDataAge = uint32(block.timestamp); + IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); + // Rescue ETH via rescuer contract. + vm.expectRevert(); + rescuer.suck( + address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + } + + function test_suck_FailsIf_challengeFails() public { + // Auth the recover contract on scribe + IAuth(address(scribe)).rely(address(rescuer)); + // Send some Eth to the scribe contract + vm.deal(address(scribe), 1 ether); + // Create a new feed + LibFeed.Feed memory feed = LibFeed.newFeed(1); + // Create registration sig + IScribe.ECDSAData memory registrationSig; + registrationSig = + feed.signECDSA(scribe.feedRegistrationMessage()); + // Construct opPokeSignature using a different feed private key, (with invalid schnorr sig) + uint32 pokeDataAge = uint32(block.timestamp); + LibFeed.Feed memory unlifted_feed = LibFeed.newFeed(2); + IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(unlifted_feed, pokeDataAge); + // Rescue ETH via rescuer contract. + vm.expectRevert(); + rescuer.suck( + address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + } + + + function testFuzz_withdraw(uint amount) public { + amount = _bound(amount, 0, 1000 ether); + // Send some Eth to the rescuer contract + vm.deal(address(rescuer), amount); + uint withdraw_amount = address(rescuer).balance; + address recipient = address(0x1234567890123456789012345678901234567890); + vm.expectEmit(); + emit Withdrawed(address(this), recipient, withdraw_amount); + rescuer.withdraw(payable(recipient), withdraw_amount); + assertEq(recipient.balance, withdraw_amount); + assertEq(withdraw_amount, amount); + } + + // Auth tests + + function test_suck_isAuthed() public { + // Deauth this on the rescuer contract + IAuth(address(rescuer)).deny(address(this)); + // Send some Eth to the scribe contract + vm.deal(address(scribe), 1 ether); + // Create a new feed + LibFeed.Feed memory feed = LibFeed.newFeed(1); + // Create registration sig + IScribe.ECDSAData memory registrationSig; + registrationSig = + feed.signECDSA(scribe.feedRegistrationMessage()); + // Construct opPokeSignature, (with invalid schnorr sig) + uint32 pokeDataAge = uint32(block.timestamp); + IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); + // Rescue ETH via rescuer contract. + vm.expectRevert( + abi.encodeWithSelector( + IAuth.NotAuthorized.selector, address(this) + ) + ); + // TODO correct revert + rescuer.suck( + address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + ); + } + + function test_withdraw_isAuthed() public { + // Deauth this on the rescuer contract + IAuth(address(rescuer)).deny(address(this)); + // Send some Eth to the rescuer contract + vm.deal(address(rescuer), 1 ether); + uint withdraw_amount = address(rescuer).balance; + address recipient = address(0x1234567890123456789012345678901234567890); + vm.expectRevert( + abi.encodeWithSelector( + IAuth.NotAuthorized.selector, address(this) + ) + ); + rescuer.withdraw(payable(recipient), withdraw_amount); + } + + + function _construct_opPokeSignature(LibFeed.Feed memory feed, uint32 pokeDataAge) private returns (IScribe.ECDSAData memory) { + IScribe.PokeData memory pokeData = IScribe.PokeData(0, pokeDataAge); + IScribe.SchnorrData memory schnorrData = + IScribe.SchnorrData(bytes32(0), address(0), hex""); + // Construct opPokeMessage. + bytes32 opPokeMessage = scribe.constructOpPokeMessage( + pokeData, schnorrData + ); + // Let feed sign opPokeMessage. + IScribe.ECDSAData memory opPokeSig = feed.signECDSA(opPokeMessage); + return opPokeSig; + } + +} From fadd75b7030b7ead44780d75e22e7d82073618fe Mon Sep 17 00:00:00 2001 From: Max Wickham Date: Thu, 3 Oct 2024 12:25:42 +0100 Subject: [PATCH 3/6] cleanup --- test/rescue/RescuerTest.sol | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/test/rescue/RescuerTest.sol b/test/rescue/RescuerTest.sol index 5b79619..f76047f 100644 --- a/test/rescue/RescuerTest.sol +++ b/test/rescue/RescuerTest.sol @@ -36,13 +36,14 @@ contract RescuerTest is Test { rescuer = new Rescuer(address(this)); } - function test_suck() public { + function testFuzz_suck(uint privKey) public { + privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); // Auth the recover contract on scribe IAuth(address(scribe)).rely(address(rescuer)); // Send some Eth to the scribe contract vm.deal(address(scribe), 1 ether); // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(1); + LibFeed.Feed memory feed = LibFeed.newFeed(privKey); // Create registration sig IScribe.ECDSAData memory registrationSig; registrationSig = @@ -64,10 +65,10 @@ contract RescuerTest is Test { emit Withdrawed(address(this), recipient, withdraw_amount); rescuer.withdraw(payable(recipient), withdraw_amount); assertEq(recipient.balance, withdraw_amount); - } - function test_suckMultiple() public { + function testFuzz_suckMultiple(uint privKey) public { + privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); address[] memory scribes = new address[](10); for (uint i = 0; i < 10; i++) { scribes[i] = address(new ScribeOptimistic(address(this), bytes32("TEST/TEST"))); @@ -77,7 +78,7 @@ contract RescuerTest is Test { vm.deal(address(scribes[i]), 1 ether); } // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(1); + LibFeed.Feed memory feed = LibFeed.newFeed(privKey); // Create registration sig IScribe.ECDSAData memory registrationSig; registrationSig = @@ -123,9 +124,11 @@ contract RescuerTest is Test { ); } - function test_suck_FailsIf_feedsLengthNotZero() public { + function testFuzz_suck_FailsIf_feedsLengthNotZero(uint privKey, uint privKeyExisting) public { + privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); + privKeyExisting = _bound(privKeyExisting, 1, LibSecp256k1.Q() - 1); // Create a new feed to lift on scribe - LibFeed.Feed memory existing_feed = LibFeed.newFeed(1); + LibFeed.Feed memory existing_feed = LibFeed.newFeed(privKeyExisting); IScribe.ECDSAData memory registrationSig; registrationSig = existing_feed.signECDSA(scribe.feedRegistrationMessage()); @@ -135,9 +138,8 @@ contract RescuerTest is Test { IAuth(address(scribe)).rely(address(rescuer)); // Send some Eth to the scribe contract vm.deal(address(scribe), 1 ether); - // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(2); // Create registration sig + LibFeed.Feed memory feed = LibFeed.newFeed(privKey); registrationSig = feed.signECDSA(scribe.feedRegistrationMessage()); // Construct opPokeSignature, (with invalid schnorr sig) @@ -186,13 +188,11 @@ contract RescuerTest is Test { assertEq(withdraw_amount, amount); } - // Auth tests + // ---------- Auth tests ---------- function test_suck_isAuthed() public { // Deauth this on the rescuer contract IAuth(address(rescuer)).deny(address(this)); - // Send some Eth to the scribe contract - vm.deal(address(scribe), 1 ether); // Create a new feed LibFeed.Feed memory feed = LibFeed.newFeed(1); // Create registration sig @@ -208,7 +208,6 @@ contract RescuerTest is Test { IAuth.NotAuthorized.selector, address(this) ) ); - // TODO correct revert rescuer.suck( address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig ); @@ -217,16 +216,13 @@ contract RescuerTest is Test { function test_withdraw_isAuthed() public { // Deauth this on the rescuer contract IAuth(address(rescuer)).deny(address(this)); - // Send some Eth to the rescuer contract - vm.deal(address(rescuer), 1 ether); - uint withdraw_amount = address(rescuer).balance; address recipient = address(0x1234567890123456789012345678901234567890); vm.expectRevert( abi.encodeWithSelector( IAuth.NotAuthorized.selector, address(this) ) ); - rescuer.withdraw(payable(recipient), withdraw_amount); + rescuer.withdraw(payable(recipient), 0); } From 14a0c202c23c6464038c333bcc0b29abf0a40595 Mon Sep 17 00:00:00 2001 From: Max Wickham Date: Fri, 4 Oct 2024 11:06:37 +0100 Subject: [PATCH 4/6] tests --- script/rescue/Rescuer.sol | 6 ++++++ test/rescue/RescuerTest.sol | 28 +++++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/script/rescue/Rescuer.sol b/script/rescue/Rescuer.sol index 6b445ea..ab05cbc 100644 --- a/script/rescue/Rescuer.sol +++ b/script/rescue/Rescuer.sol @@ -81,6 +81,12 @@ contract Rescuer is Auth { } /// @notice Rescues ETH from multiple ScribeOptimistic instances `opScribes`. + /// + /// @dev Note that `opScribes` MUST be deactivated. + /// @dev Note that validator key pair SHALL be only used once and generated + /// via a CSPRNG. + /// + /// @dev Only callable by auth'ed address. function suck( address[] memory opScribes, LibSecp256k1.Point memory pubKey, diff --git a/test/rescue/RescuerTest.sol b/test/rescue/RescuerTest.sol index f76047f..43edd62 100644 --- a/test/rescue/RescuerTest.sol +++ b/test/rescue/RescuerTest.sol @@ -34,8 +34,12 @@ contract RescuerTest is Test { scribe = new ScribeOptimistic(address(this), bytes32("TEST/TEST")); IScribeOptimistic(scribe).setMaxChallengeReward(type(uint).max); rescuer = new Rescuer(address(this)); + // Auth the recover contract on scribe + IAuth(address(scribe)).rely(address(rescuer)); } + // -- Test: Suck -- + function testFuzz_suck(uint privKey) public { privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); // Auth the recover contract on scribe @@ -65,12 +69,13 @@ contract RescuerTest is Test { emit Withdrawed(address(this), recipient, withdraw_amount); rescuer.withdraw(payable(recipient), withdraw_amount); assertEq(recipient.balance, withdraw_amount); + assertEq(recipient.balance, 1 ether); } function testFuzz_suckMultiple(uint privKey) public { privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); address[] memory scribes = new address[](10); - for (uint i = 0; i < 10; i++) { + for (uint i; i < scribes.len; i++) { scribes[i] = address(new ScribeOptimistic(address(this), bytes32("TEST/TEST"))); IScribeOptimistic(scribes[i]).setMaxChallengeReward(type(uint).max); // Auth the recover contract on scribe @@ -87,7 +92,7 @@ contract RescuerTest is Test { uint32 pokeDataAge = uint32(block.timestamp); IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); // Rescue ETH via rescuer contract. - for (uint i = 0; i < 10; i++) { + for (uint i; i < scribes.len; i++) { vm.expectEmit(); emit Recovered(address(this), address(scribes[i]), 1 ether); } @@ -95,13 +100,16 @@ contract RescuerTest is Test { scribes, feed.pubKey, registrationSig, pokeDataAge, opPokeSig ); // Withdraw the eth - uint current_balance = address(this).balance; - uint withdraw_amount = address(rescuer).balance; address recipient = address(0x1234567890123456789012345678901234567890); - vm.expectEmit(); - emit Withdrawed(address(this), recipient, withdraw_amount); - rescuer.withdraw(payable(recipient), withdraw_amount); - assertEq(recipient.balance, withdraw_amount); + for (uint i; i < scribes.len; i++){ + uint current_balance = address(this).balance; + uint withdraw_amount = address(rescuer).balance; + vm.expectEmit(); + emit Withdrawed(address(this), recipient, withdraw_amount); + rescuer.withdraw(payable(recipient), withdraw_amount); + assertEq(recipient.balance, withdraw_amount); + } + assertEq(recipient.balance, scribes.len * (1 ether)); } @@ -174,6 +182,7 @@ contract RescuerTest is Test { ); } + // -- Test: Withdraw -- function testFuzz_withdraw(uint amount) public { amount = _bound(amount, 0, 1000 ether); @@ -188,7 +197,7 @@ contract RescuerTest is Test { assertEq(withdraw_amount, amount); } - // ---------- Auth tests ---------- + // -- Test: Auth -- function test_suck_isAuthed() public { // Deauth this on the rescuer contract @@ -225,6 +234,7 @@ contract RescuerTest is Test { rescuer.withdraw(payable(recipient), 0); } + // -- Helpers -- function _construct_opPokeSignature(LibFeed.Feed memory feed, uint32 pokeDataAge) private returns (IScribe.ECDSAData memory) { IScribe.PokeData memory pokeData = IScribe.PokeData(0, pokeDataAge); From aac35c43e86eb7fab29913520c8bfa182bc520a6 Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 10 Oct 2024 13:00:16 +0200 Subject: [PATCH 5/6] rescue: Finishes tests --- script/rescue/Rescuer.sol | 40 +---- test/rescue/RescuerTest.sol | 331 ++++++++++++++++++------------------ 2 files changed, 163 insertions(+), 208 deletions(-) diff --git a/script/rescue/Rescuer.sol b/script/rescue/Rescuer.sol index ab05cbc..69e7ff2 100644 --- a/script/rescue/Rescuer.sol +++ b/script/rescue/Rescuer.sol @@ -77,40 +77,8 @@ contract Rescuer is Auth { uint32 pokeDataAge, IScribe.ECDSAData memory opPokeSig ) external auth { - _suck(opScribe, pubKey, registrationSig, pokeDataAge, opPokeSig); - } - - /// @notice Rescues ETH from multiple ScribeOptimistic instances `opScribes`. - /// - /// @dev Note that `opScribes` MUST be deactivated. - /// @dev Note that validator key pair SHALL be only used once and generated - /// via a CSPRNG. - /// - /// @dev Only callable by auth'ed address. - function suck( - address[] memory opScribes, - LibSecp256k1.Point memory pubKey, - IScribe.ECDSAData memory registrationSig, - uint32 pokeDataAge, - IScribe.ECDSAData memory opPokeSig - ) external auth { - for (uint i = 0; i < opScribes.length; i++) { - _suck(opScribes[i], pubKey, registrationSig, pokeDataAge, opPokeSig); - } - } - - function _suck( - address opScribe, - LibSecp256k1.Point memory pubKey, - IScribe.ECDSAData memory registrationSig, - uint32 pokeDataAge, - IScribe.ECDSAData memory opPokeSig - ) internal { require(IAuth(opScribe).authed(address(this))); - address validator = pubKey.toAddress(); - uint8 validatorId = uint8(uint(uint160(validator)) >> 152); - uint balanceBefore = address(this).balance; // Fail if instance has feeds lifted, ie is not deactivated. @@ -134,11 +102,7 @@ contract Rescuer is Auth { IScribeOptimistic(opScribe).opPoke(pokeData, schnorrSig, opPokeSig); // Perform opChallenge. - bool ok = IScribeOptimistic(opScribe).opChallenge(schnorrSig); - require(ok); - - // Drop validator again. - IScribe(opScribe).drop(validatorId); + IScribeOptimistic(opScribe).opChallenge(schnorrSig); // Compute amount of ETH received as challenge reward. uint amount = address(this).balance - balanceBefore; @@ -146,6 +110,4 @@ contract Rescuer is Auth { // Emit event. emit Recovered(msg.sender, opScribe, amount); } - - } diff --git a/test/rescue/RescuerTest.sol b/test/rescue/RescuerTest.sol index 43edd62..8cdff93 100644 --- a/test/rescue/RescuerTest.sol +++ b/test/rescue/RescuerTest.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -import {console2} from "forge-std/console2.sol"; import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; import {IAuth} from "chronicle-std/auth/IAuth.sol"; @@ -20,6 +20,7 @@ contract RescuerTest is Test { using LibSecp256k1 for LibSecp256k1.Point; using LibFeed for LibFeed.Feed; + // Events copied from Rescuer. event Recovered( address indexed caller, address indexed opScribe, uint amount ); @@ -27,226 +28,218 @@ contract RescuerTest is Test { address indexed caller, address indexed receiver, uint amount ); - IScribeOptimistic private scribe; Rescuer private rescuer; + IScribeOptimistic private opScribe; + + bytes32 internal FEED_REGISTRATION_MESSAGE; function setUp() public { - scribe = new ScribeOptimistic(address(this), bytes32("TEST/TEST")); - IScribeOptimistic(scribe).setMaxChallengeReward(type(uint).max); + opScribe = new ScribeOptimistic(address(this), bytes32("TEST/TEST")); + rescuer = new Rescuer(address(this)); - // Auth the recover contract on scribe - IAuth(address(scribe)).rely(address(rescuer)); + + // Note to auth rescuer on opScribe. + IAuth(address(opScribe)).rely(address(rescuer)); + + // Note to let opScribe have a non-zero ETH balance. + vm.deal(address(opScribe), 1 ether); + + // Cache constants. + FEED_REGISTRATION_MESSAGE = opScribe.feedRegistrationMessage(); } // -- Test: Suck -- - function testFuzz_suck(uint privKey) public { - privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); - // Auth the recover contract on scribe - IAuth(address(scribe)).rely(address(rescuer)); - // Send some Eth to the scribe contract - vm.deal(address(scribe), 1 ether); - // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(privKey); - // Create registration sig - IScribe.ECDSAData memory registrationSig; - registrationSig = - feed.signECDSA(scribe.feedRegistrationMessage()); - // Construct opPokeSignature, (with invalid schnorr sig) + function testFuzz_suck(uint privKeySeed) public { + // Create new feed from privKeySeed. + LibFeed.Feed memory feed = + LibFeed.newFeed(_bound(privKeySeed, 1, LibSecp256k1.Q() - 1)); + + // Construct opPoke signature with invalid Schnorr signature. uint32 pokeDataAge = uint32(block.timestamp); - IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); - // Rescue ETH via rescuer contract. - vm.expectEmit(); - emit Recovered(address(this), address(scribe), 1 ether); - rescuer.suck( - address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig - ); - // Withdraw the eth - uint current_balance = address(this).balance; - uint withdraw_amount = address(rescuer).balance; - address recipient = address(0x1234567890123456789012345678901234567890); + IScribe.ECDSAData memory opPokeSig = + _constructOpPokeSig(feed, pokeDataAge); + vm.expectEmit(); - emit Withdrawed(address(this), recipient, withdraw_amount); - rescuer.withdraw(payable(recipient), withdraw_amount); - assertEq(recipient.balance, withdraw_amount); - assertEq(recipient.balance, 1 ether); - } + emit Recovered(address(this), address(opScribe), 1 ether); - function testFuzz_suckMultiple(uint privKey) public { - privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); - address[] memory scribes = new address[](10); - for (uint i; i < scribes.len; i++) { - scribes[i] = address(new ScribeOptimistic(address(this), bytes32("TEST/TEST"))); - IScribeOptimistic(scribes[i]).setMaxChallengeReward(type(uint).max); - // Auth the recover contract on scribe - IAuth(address(scribes[i])).rely(address(rescuer)); - vm.deal(address(scribes[i]), 1 ether); - } - // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(privKey); - // Create registration sig - IScribe.ECDSAData memory registrationSig; - registrationSig = - feed.signECDSA(scribe.feedRegistrationMessage()); - // Construct opPokeSignature, (with invalid schnorr sig) - uint32 pokeDataAge = uint32(block.timestamp); - IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); - // Rescue ETH via rescuer contract. - for (uint i; i < scribes.len; i++) { - vm.expectEmit(); - emit Recovered(address(this), address(scribes[i]), 1 ether); - } rescuer.suck( - scribes, feed.pubKey, registrationSig, pokeDataAge, opPokeSig + address(opScribe), + feed.pubKey, + feed.signECDSA(FEED_REGISTRATION_MESSAGE), + pokeDataAge, + opPokeSig ); - // Withdraw the eth - address recipient = address(0x1234567890123456789012345678901234567890); - for (uint i; i < scribes.len; i++){ - uint current_balance = address(this).balance; - uint withdraw_amount = address(rescuer).balance; - vm.expectEmit(); - emit Withdrawed(address(this), recipient, withdraw_amount); - rescuer.withdraw(payable(recipient), withdraw_amount); - assertEq(recipient.balance, withdraw_amount); - } - assertEq(recipient.balance, scribes.len * (1 ether)); + // Verify balances. + assertEq(address(opScribe).balance, 0); + assertEq(address(rescuer).balance, 1 ether); + + // Verify feed got kicked. + assertFalse(opScribe.feeds(feed.pubKey.toAddress())); } - function test_suck_FailsIf_rescuerNotAuthed() public { - // Send some Eth to the scribe contract - vm.deal(address(scribe), 1 ether); - // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(1); - // Create registration sig - IScribe.ECDSAData memory registrationSig; - registrationSig = - feed.signECDSA(scribe.feedRegistrationMessage()); - // Construct opPokeSignature, (with invalid schnorr sig) + function testFuzz_suck_FailsIf_RescuerNotAuthedOnOpScribe(uint privKeySeed) + public + { + // Create new feed from privKeySeed. + LibFeed.Feed memory feed = + LibFeed.newFeed(_bound(privKeySeed, 1, LibSecp256k1.Q() - 1)); + + // Construct opPoke signature with invalid Schnorr signature. uint32 pokeDataAge = uint32(block.timestamp); - IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); - // Rescue ETH via rescuer contract. + IScribe.ECDSAData memory opPokeSig = + _constructOpPokeSig(feed, pokeDataAge); + + // Deny rescuer on opScribe. + IAuth(address(opScribe)).deny(address(rescuer)); + + // Expect rescue to fail. vm.expectRevert(); rescuer.suck( - address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + address(opScribe), + feed.pubKey, + feed.signECDSA(FEED_REGISTRATION_MESSAGE), + pokeDataAge, + opPokeSig ); } - function testFuzz_suck_FailsIf_feedsLengthNotZero(uint privKey, uint privKeyExisting) public { - privKey = _bound(privKey, 1, LibSecp256k1.Q() - 1); - privKeyExisting = _bound(privKeyExisting, 1, LibSecp256k1.Q() - 1); - // Create a new feed to lift on scribe - LibFeed.Feed memory existing_feed = LibFeed.newFeed(privKeyExisting); - IScribe.ECDSAData memory registrationSig; - registrationSig = - existing_feed.signECDSA(scribe.feedRegistrationMessage()); - // Lift validator. - scribe.lift(existing_feed.pubKey, registrationSig); - // Auth the recover contract on scribe - IAuth(address(scribe)).rely(address(rescuer)); - // Send some Eth to the scribe contract - vm.deal(address(scribe), 1 ether); - // Create registration sig - LibFeed.Feed memory feed = LibFeed.newFeed(privKey); - registrationSig = - feed.signECDSA(scribe.feedRegistrationMessage()); - // Construct opPokeSignature, (with invalid schnorr sig) + function testFuzz_suck_FailsIf_OpScribeNotDeactivated( + uint privKeySeed, + uint privKeyLiftedSeed + ) public { + // Create new feeds from seeds + LibFeed.Feed memory feed = + LibFeed.newFeed(_bound(privKeySeed, 1, LibSecp256k1.Q() - 1)); + LibFeed.Feed memory feedLifted = + LibFeed.newFeed(_bound(privKeyLiftedSeed, 1, LibSecp256k1.Q() - 1)); + + // Lift feedLifted. + opScribe.lift( + feedLifted.pubKey, + feedLifted.signECDSA(opScribe.feedRegistrationMessage()) + ); + + // Construct opPoke signature with invalid Schnorr signature. uint32 pokeDataAge = uint32(block.timestamp); - IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); - // Rescue ETH via rescuer contract. + IScribe.ECDSAData memory opPokeSig = + _constructOpPokeSig(feed, pokeDataAge); + + // Expect rescue to fail. vm.expectRevert(); rescuer.suck( - address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + address(opScribe), + feed.pubKey, + feed.signECDSA(FEED_REGISTRATION_MESSAGE), + pokeDataAge, + opPokeSig ); } - function test_suck_FailsIf_challengeFails() public { - // Auth the recover contract on scribe - IAuth(address(scribe)).rely(address(rescuer)); - // Send some Eth to the scribe contract - vm.deal(address(scribe), 1 ether); - // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(1); - // Create registration sig - IScribe.ECDSAData memory registrationSig; - registrationSig = - feed.signECDSA(scribe.feedRegistrationMessage()); - // Construct opPokeSignature using a different feed private key, (with invalid schnorr sig) - uint32 pokeDataAge = uint32(block.timestamp); - LibFeed.Feed memory unlifted_feed = LibFeed.newFeed(2); - IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(unlifted_feed, pokeDataAge); - // Rescue ETH via rescuer contract. - vm.expectRevert(); + function test_suck_isAuthProtected() public { + vm.prank(address(0xbeef)); + vm.expectRevert( + abi.encodeWithSelector( + IAuth.NotAuthorized.selector, address(0xbeef) + ) + ); rescuer.suck( - address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig + address(opScribe), + LibSecp256k1.ZERO_POINT(), + IScribe.ECDSAData(uint8(0), bytes32(0), bytes32(0)), + uint32(0), + IScribe.ECDSAData(uint8(0), bytes32(0), bytes32(0)) ); } // -- Test: Withdraw -- - function testFuzz_withdraw(uint amount) public { - amount = _bound(amount, 0, 1000 ether); - // Send some Eth to the rescuer contract - vm.deal(address(rescuer), amount); - uint withdraw_amount = address(rescuer).balance; - address recipient = address(0x1234567890123456789012345678901234567890); + function testFuzz_withdraw_ToEOA( + address payable receiver, + uint balance, + uint withdrawal + ) public { + vm.assume(receiver.code.length == 0); + vm.assume(balance >= withdrawal); + + // Let rescuer have ETH balance. + vm.deal(address(rescuer), balance); + vm.expectEmit(); - emit Withdrawed(address(this), recipient, withdraw_amount); - rescuer.withdraw(payable(recipient), withdraw_amount); - assertEq(recipient.balance, withdraw_amount); - assertEq(withdraw_amount, amount); + emit Withdrawed(address(this), receiver, withdrawal); + + rescuer.withdraw(receiver, withdrawal); + + assertEq(address(rescuer).balance, balance - withdrawal); + assertEq(receiver.balance, withdrawal); } - // -- Test: Auth -- - - function test_suck_isAuthed() public { - // Deauth this on the rescuer contract - IAuth(address(rescuer)).deny(address(this)); - // Create a new feed - LibFeed.Feed memory feed = LibFeed.newFeed(1); - // Create registration sig - IScribe.ECDSAData memory registrationSig; - registrationSig = - feed.signECDSA(scribe.feedRegistrationMessage()); - // Construct opPokeSignature, (with invalid schnorr sig) - uint32 pokeDataAge = uint32(block.timestamp); - IScribe.ECDSAData memory opPokeSig = _construct_opPokeSignature(feed, pokeDataAge); - // Rescue ETH via rescuer contract. - vm.expectRevert( - abi.encodeWithSelector( - IAuth.NotAuthorized.selector, address(this) - ) - ); - rescuer.suck( - address(scribe), feed.pubKey, registrationSig, pokeDataAge, opPokeSig - ); + function test_withdraw_ToContract(uint balance, uint withdrawal) public { + vm.assume(balance >= withdrawal); + + // Let rescuer have ETH balance. + vm.deal(address(rescuer), balance); + + // Deploy ETH receiver. + ETHReceiver receiver = new ETHReceiver(); + + vm.expectEmit(); + emit Withdrawed(address(this), address(receiver), withdrawal); + + rescuer.withdraw(payable(address(receiver)), withdrawal); + + assertEq(address(rescuer).balance, balance - withdrawal); + assertEq(address(receiver).balance, withdrawal); } - function test_withdraw_isAuthed() public { - // Deauth this on the rescuer contract - IAuth(address(rescuer)).deny(address(this)); - address recipient = address(0x1234567890123456789012345678901234567890); + function test_withdraw_FailsIf_ETHTransferFails(uint balance, uint withdrawal) public { + vm.assume(balance >= withdrawal); + + // Let rescuer have ETH balance. + vm.deal(address(rescuer), balance); + + // Deploy non ETH receiver. + NotETHReceiver receiver = new NotETHReceiver(); + + vm.expectRevert(); + rescuer.withdraw(payable(address(receiver)), withdrawal); + } + + function test_withdraw_isAuthProtected() public { + vm.prank(address(0xbeef)); vm.expectRevert( abi.encodeWithSelector( - IAuth.NotAuthorized.selector, address(this) + IAuth.NotAuthorized.selector, address(0xbeef) ) ); - rescuer.withdraw(payable(recipient), 0); + rescuer.withdraw(payable(address(this)), 0); } // -- Helpers -- - function _construct_opPokeSignature(LibFeed.Feed memory feed, uint32 pokeDataAge) private returns (IScribe.ECDSAData memory) { + function _constructOpPokeSig(LibFeed.Feed memory feed, uint32 pokeDataAge) + internal + view + returns (IScribe.ECDSAData memory) + { + // Construct pokeData with zero val and given age. IScribe.PokeData memory pokeData = IScribe.PokeData(0, pokeDataAge); + + // Construct invalid Schnorr signature. IScribe.SchnorrData memory schnorrData = IScribe.SchnorrData(bytes32(0), address(0), hex""); + // Construct opPokeMessage. - bytes32 opPokeMessage = scribe.constructOpPokeMessage( - pokeData, schnorrData - ); + bytes32 opPokeMessage = + opScribe.constructOpPokeMessage(pokeData, schnorrData); + // Let feed sign opPokeMessage. - IScribe.ECDSAData memory opPokeSig = feed.signECDSA(opPokeMessage); - return opPokeSig; + return feed.signECDSA(opPokeMessage); } +} +contract NotETHReceiver {} +contract ETHReceiver { + receive() external payable {} } From bb4f7d8dcd955ce1fada70643f4df4629ba409df Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 10 Oct 2024 13:01:51 +0200 Subject: [PATCH 6/6] test: Fix formatting --- test/rescue/RescuerTest.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/rescue/RescuerTest.sol b/test/rescue/RescuerTest.sol index 8cdff93..1e2cd84 100644 --- a/test/rescue/RescuerTest.sol +++ b/test/rescue/RescuerTest.sol @@ -193,7 +193,10 @@ contract RescuerTest is Test { assertEq(address(receiver).balance, withdrawal); } - function test_withdraw_FailsIf_ETHTransferFails(uint balance, uint withdrawal) public { + function test_withdraw_FailsIf_ETHTransferFails( + uint balance, + uint withdrawal + ) public { vm.assume(balance >= withdrawal); // Let rescuer have ETH balance. @@ -240,6 +243,7 @@ contract RescuerTest is Test { } contract NotETHReceiver {} + contract ETHReceiver { receive() external payable {} }