Skip to content

Commit

Permalink
script: Adds ETH rescue tooling for deactivated opScribes
Browse files Browse the repository at this point in the history
  • Loading branch information
pmerkleplant committed Oct 2, 2024
1 parent 05a60a5 commit bf65e63
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 4 deletions.
4 changes: 2 additions & 2 deletions script/Scribe.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
81 changes: 79 additions & 2 deletions script/ScribeOptimistic.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -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
);
}
}
120 changes: 120 additions & 0 deletions script/rescue/Rescuer.sol
Original file line number Diff line number Diff line change
@@ -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 [email protected]
*/
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);
}
}

0 comments on commit bf65e63

Please sign in to comment.