Compare commits

...

7 Commits

Author SHA1 Message Date
Max Wolff
487f4f2af4 wip cli 2023-06-27 21:42:24 -07:00
Max Wolff
0a57747085 add wip cli 2023-06-27 21:36:44 -07:00
Max Wolff
e59a1d4fba demo 2023-06-19 00:40:26 -07:00
Max Wolff
17bbb929b7 debug v param. sigs now work 2023-06-16 02:04:58 -07:00
Max Wolff
c95e0c1782 wip 2023-06-13 10:13:55 -07:00
Max Wolff
091da32936 add deployment script 2023-06-02 16:11:27 -07:00
Max Wolff
6155612eec wip 2023-05-31 00:33:49 -07:00
23 changed files with 1051 additions and 39 deletions

3
.gitmodules vendored
View File

@@ -13,3 +13,6 @@
[submodule "contracts/lib/solmate"]
path = contracts/lib/solmate
url = https://github.com/rari-capital/solmate
[submodule "contracts/lib/safe-contracts"]
path = contracts/lib/safe-contracts
url = https://github.com/safe-global/safe-contracts

3
contracts/admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
dist

23
contracts/admin/README.md Normal file
View File

@@ -0,0 +1,23 @@
# admin cli
WIP
provides commands to generate calldata to then paste into `cast sign` or similar tools. No cast sign raw tx exists, and want to give users ability to
chose what method they sign with, so prefer not signing the tx in this cli tool.
example (hypothetical) usage:
- npm link
- admin-cli approveHash --network testnet --domain L1 --targetAddress 0x0 --targetCalldata 0x0
{
to: 0x1234,
data: 0x1234,
functionSig: "approveHash(bytes32)"
}
Flow:
- first, approve desired transaction (schedules transaction in Timelock) in SAFE with approveHash()
- second, someone collects all the signers and sends executeTransaction()
- third, someone calls execute() on the Timelock. this actually sends the transaction throught the forwarder and executes the call

11
contracts/admin/abis.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -ue
# This script is used to generate the typechain artifacts for the contracts
mkdir -p abis types
cat ../artifacts/src/Safe.sol/Safe.json | jq .abi >> abis/safe.json
cat ../artifacts/src/TimelockController.sol/TimelockController.json | jq .abi >> abis/timelock.json
cat ../artifacts/src/Forwarder.sol/Forwarder.json | jq .abi >> abis/forwarder.json
npx typechain --target=ethers-v6 "abis/*.json"

2
contracts/admin/bin/index.js Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
require("../dist/cli.js");

57
contracts/admin/cli.ts Normal file
View File

@@ -0,0 +1,57 @@
import yargs from "yargs";
import { ethers } from "ethers";
import { DomainDeployment, getConfig } from "./config";
import { approveHash } from "./tx";
// eslint-disable-next-line no-unused-expressions
yargs
.command(
"approveHash",
"approve transaction hash in SAFE",
(yargs) =>
yargs
.options({
network: {
alias: "n",
describe: "name of network config to use, eg: {mainnet | goerli | testnet}",
string: true,
},
domain: {
describe: "L1 or L2",
string: true,
coerce: (arg) => arg.toUpperCase(),
},
targetAddress: {
describe: "address of contract to call",
string: true,
},
targetCalldata: {
describe: "calldata to send to contract",
string: true,
},
})
.check((argv) => {
if (!(argv.targetAddress && argv.targetCalldata) && !(argv.network && argv.domain)) {
throw new Error("Must provide network, domain, targetAddress and targetCalldata");
}
return true; // If no error was thrown, validation passed and you can return true
}),
async (argv) => {
// todo: validate
const targetAddress = ethers.getAddress(argv.targetAddress!);
const targetCalldata = argv.targetCalldata!;
console.log("using target value from args: ", { targetAddress, targetCalldata });
const conf = getConfig(argv.network!, argv.domain!);
const fragment = await approveHash(
targetAddress,
ethers.getBytes(targetCalldata),
conf.ScrollSafeAddress,
conf.ForwarderAddress,
conf.ScrollTimelockAddress
);
console.log(fragment);
}
)
.help().argv;

49
contracts/admin/config.ts Normal file
View File

@@ -0,0 +1,49 @@
export interface DomainDeployment {
ForwarderAddress: string;
ScrollSafeAddress: string;
ScrollTimelockAddress: string;
CouncilSafeAddress: string;
CouncilTimelockAddress: string;
}
export interface Deployment {
L1: DomainDeployment;
L2: DomainDeployment;
}
export interface Config {
[key: string]: Deployment;
}
const config: Config = {
testnet: {
L1: {
ForwarderAddress: "0x0000000000000000000000000000000000000000",
ScrollSafeAddress: "0x0000000000000000000000000000000000000000",
ScrollTimelockAddress: "0x0000000000000000000000000000000000000000",
CouncilSafeAddress: "0x0000000000000000000000000000000000000000",
CouncilTimelockAddress: "0x0000000000000000000000000000000000000000",
},
L2: {
ForwarderAddress: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
ScrollSafeAddress: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
ScrollTimelockAddress: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
CouncilSafeAddress: "0x0000000000000000000000000000000000000000",
CouncilTimelockAddress: "0x0000000000000000000000000000000000000000",
},
},
};
export const getConfig = (network: string, domain: string): DomainDeployment => {
if (network in config) {
if (domain in config[network]) {
return config[network][domain as keyof Deployment];
} else {
throw new Error(`Invalid domain: ${domain}`);
}
} else {
throw new Error(`Invalid network: ${network}`);
}
};

View File

@@ -0,0 +1,19 @@
{
"name": "admin-cli",
"bin": {
"admin-cli": "./bin/index.js"
},
"main": "bin/index.js",
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"dependencies": {
"ethers": "^6.6.1",
"yargs": "^17.7.2"
},
"devDependencies": {
"@typechain/ethers-v6": "^0.4.0",
"@types/yargs": "^17.0.24"
}
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"declaration": true
}
}

113
contracts/admin/tx.ts Normal file
View File

@@ -0,0 +1,113 @@
import { ethers } from "ethers";
import {
Safe__factory,
Safe,
Forwarder__factory,
Forwarder,
Timelock__factory,
Timelock,
} from "./types/ethers-contracts";
export interface RawTxFragment {
to: string;
callData: string;
functionSig: string;
}
async function execTransaction(wallet: ethers.Wallet, safeContract: Safe, calldata: string, senders: string[]) {
// ethers.AbiCoder.encode(
// Safe__factory.abi
let signatures = "0x0000000000000000000000000000000000000000";
for (let i = 0; i < senders.length; i++) {
signatures += encodeAddress(senders[i]);
}
await safeContract
.connect(wallet)
.execTransaction(
"0x0000000000000000000000000000000000000000",
0,
calldata,
0,
0,
0,
0,
ethers.ZeroAddress,
ethers.ZeroAddress,
signatures,
{ gasLimit: 1000000 }
);
}
export async function approveHash(
targetAddress: ethers.AddressLike,
targetCalldata: ethers.BytesLike,
safeAddress: ethers.AddressLike,
forwarderAddress: ethers.AddressLike,
timelockAddress: ethers.AddressLike
): Promise<RawTxFragment> {
// either implement getTransactionHash in JS or make RPC call to get hash
const provider = new ethers.JsonRpcProvider("http://localhost:1234");
const safeContract = Safe__factory.connect(safeAddress.toString(), provider);
const forwarderContract = Forwarder__factory.connect(forwarderAddress.toString());
const timelockContract = Timelock__factory.connect(timelockAddress.toString());
// const targetCalldata = targetContract.interface.encodeFunctionData("err");
const forwarderCalldata = forwarderContract.interface.encodeFunctionData("forward", [
targetAddress.toString(),
targetCalldata,
]);
const timelockScheduleCalldata = timelockContract.interface.encodeFunctionData("schedule", [
forwarderAddress.toString(),
0,
forwarderCalldata,
ethers.ZeroHash,
ethers.ZeroHash,
0,
]);
const txHash = await safeContract.getTransactionHash(
timelockAddress.toString(),
0,
timelockScheduleCalldata,
0,
0,
0,
0,
ethers.ZeroAddress,
ethers.ZeroAddress,
0
);
return {
to: safeAddress.toString(),
callData: txHash,
functionSig: "approveHash(bytes32)",
};
}
// await safeContract.checkNSignatures(scheduleSafeTxHash, ethers.arrayify("0x00"), sigSchedule, 1);
// await timelockContract
// .connect(wallet)
// .execute(L2_FORWARDER_ADDR, 0, forwarderCalldata, ethers.HashZero, ethers.HashZero, {
// gasLimit: 1000000,
// });
// safe takes address as part of the signature
function encodeAddress(address: string) {
const r = ethers.zeroPadValue(address, 32);
const s = ethers.zeroPadValue("0x00", 32);
const v = "0x01";
return ethers.toBeHex(ethers.concat([r, s, v])).slice(-2);
}
// add 4 to the v byte at the end of the signature
function editSig(sig: string) {
const v = parseInt(sig.slice(-2), 16);
const newV = v + 4;
const newSig = sig.slice(0, -2) + newV.toString(16);
return newSig;
}
console.log(encodeAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"));
module.exports = {
approveHash,
};

View File

@@ -3,7 +3,7 @@ src = 'src' # the source directory
test = 'src/test' # the test directory
script = 'scripts' # the script directory
out = 'artifacts/src' # the output directory (for artifacts)
libs = [] # a list of library directories
libs = ["lib"] # the library directory
remappings = [] # a list of remappings
libraries = [] # a list of deployed libraries to link against
cache = true # whether to cache builds or not

26
contracts/scripts/deploy.sh Executable file
View File

@@ -0,0 +1,26 @@
#/bin/sh
set -uex
PID=$(lsof -t -i:1234)
echo $PID
kill $PID
export L2_DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PORT=1234
# deploys a local instance of the contracts
anvil --port $PORT &
while ! lsof -i :$PORT
do
echo "...waiting for anvil"
sleep 1
done
echo "started anvil"
forge script ./foundry/DeployL2AdminContracts.s.sol:DeployL2AdminContracts --rpc-url http://localhost:1234 --legacy --broadcast -vvvv
npx ts-node ./encode.ts
echo "deployment success"

74
contracts/scripts/encode.sh Executable file
View File

@@ -0,0 +1,74 @@
#/bin/sh
set -uex
# does not work due to V recovery bit being off
L2_COUNCIL_SAFE_ADDR=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
L2_COUNCIL_TIMELOCK_ADDR=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
L2_SCROLL_SAFE_ADDR=0xa513E6E4b8f2a923D98304ec87F64353C4D5C853
L2_SCROLL_TIMELOCK_ADDR=0x8A791620dd6260079BF849Dc5567aDC3F2FdC318
L2_FORWARDER_ADDR=0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0
L2_TARGET_ADDR=0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
# 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
L2_DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
ZERO_BYTES=0x0000000000000000000000000000000000000000
# sign tx hash for timelock schedule call
ADMIN_CALLDATA=$(cast calldata "err()")
FORWARDER_CALLDATA=$(cast calldata "forward(address,bytes)" $L2_FORWARDER_ADDR $ADMIN_CALLDATA)
TIMELOCK_SCHEDULE_CALLDATA=$(cast calldata "schedule(address,uint256,bytes,bytes32,bytes32,uint256)" $L2_FORWARDER_ADDR 0 $FORWARDER_CALLDATA 0x0 0x0 0x0)
SAFE_TX_HASH=$(cast call -r http://localhost:1234 $L2_SCROLL_SAFE_ADDR "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)" \
$L2_SCROLL_TIMELOCK_ADDR 0 $TIMELOCK_SCHEDULE_CALLDATA 0 0 0 0 $ZERO_BYTES $ZERO_BYTES 0)
SAFE_SIG=$(cast wallet sign --private-key $L2_DEPLOYER_PRIVATE_KEY $SAFE_TX_HASH | awk '{print $2}')
# echo $SAFE_SIG
# echo $SAFE_TX_HASH
# send safe tx to schedule the call
cast send -c 31337 --legacy --private-key $L2_DEPLOYER_PRIVATE_KEY -r http://localhost:1234 --gas-limit 1000000 $L2_SCROLL_SAFE_ADDR "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)" \
$L2_SCROLL_TIMELOCK_ADDR 0 $TIMELOCK_SCHEDULE_CALLDATA 0 0 0 0 $ZERO_BYTES $ZERO_BYTES $SAFE_SIG
# function encodeTransactionData(
# address to,
# uint256 value,
# bytes calldata data,
# Enum.Operation operation,
# uint256 safeTxGas,
# uint256 baseGas,
# uint256 gasPrice,
# address gasToken,
# address refundReceiver,
# uint256 _nonce
# function execTransaction(
# address to,
# uint256 value,
# bytes calldata data,
# Enum.Operation operation,
# uint256 safeTxGas,
# uint256 baseGas,
# uint256 gasPrice,
# address gasToken,
# address payable refundReceiver,
# bytes memory signatures
exit 0
# /////////////// 2nd tx ///////////////
# sign tx hash for execute call
TIMELOCK_EXECUTE_CALLDATA=$(cast calldata "execute(address,uint256,bytes,bytes32,bytes32)" $L2_FORWARDER_ADDR 0 $FORWARDER_CALLDATA 0x0 0x0)
SAFE_TX_HASH_=$(cast call -r http://localhost:1234 $L2_SCROLL_SAFE_ADDR "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)" \
$L2_SCROLL_TIMELOCK_ADDR 0 $TIMELOCK_SCHEDULE_CALLDATA 0 0 0 0 $ZERO_BYTES $ZERO_BYTES 0)
SAFE_SIG=$(cast wallet sign --private-key $L2_DEPLOYER_PRIVATE_KEY $SAFE_TX_HASH | awk '{print $2}')
# send safe tx to execute the call
cast send -c 31337 --legacy --private-key $L2_DEPLOYER_PRIVATE_KEY -r http://localhost:1234 --gas-limit 1000000 $L2_SCROLL_SAFE_ADDR "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)" \
$L2_SCROLL_TIMELOCK_ADDR 0 $TIMELOCK_EXECUTE_CALLDATA 0 0 0 0 $ZERO_BYTES $ZERO_BYTES $SAFE_SIG
echo "DONE"

102
contracts/scripts/encode.ts Normal file
View File

@@ -0,0 +1,102 @@
import { ethers } from "ethers";
import { Safeabi__factory, Forwarder__factory, Target__factory, Timelock__factory } from "../safeAbi";
const L2_SCROLL_SAFE_ADDR = "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853";
const L2_SCROLL_TIMELOCK_ADDR = "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318";
const L2_FORWARDER_ADDR = "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0";
const L2_TARGET_ADDR = "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82";
const L2_DEPLOYER_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
/*
TODO:
* read from env
* use approve hash flow
* read nonce from safe
* split script into schedule and execute
* add gas limit
* document how to use
* how to get addresses from deployment?
* get abis in a reasonable way
*/
/*
to get safe abi
* forge build
* cat artifacts/src/Safe.sol/Safe.json| jq .abi >> safeabi.json
* mkdir safeAbi
* npx typechain --target=ethers-v5 safeabi.json --out-dir safeAbi
repeat for forwarder, timelock, target
*/
async function main() {
const provider = new ethers.providers.JsonRpcProvider("http://localhost:1234");
const wallet = new ethers.Wallet(L2_DEPLOYER_PRIVATE_KEY, provider);
const safeContract = Safeabi__factory.connect(L2_SCROLL_SAFE_ADDR, provider);
const forwarderContract = Forwarder__factory.connect(L2_FORWARDER_ADDR, provider);
const timelockContract = Timelock__factory.connect(L2_SCROLL_TIMELOCK_ADDR, provider);
const targetContract = Target__factory.connect(L2_TARGET_ADDR, provider);
const targetCalldata = targetContract.interface.encodeFunctionData("err");
const forwarderCalldata = forwarderContract.interface.encodeFunctionData("forward", [L2_TARGET_ADDR, targetCalldata]);
const timelockScheduleCalldata = timelockContract.interface.encodeFunctionData("schedule", [
L2_FORWARDER_ADDR,
0,
forwarderCalldata,
ethers.constants.HashZero,
ethers.constants.HashZero,
0,
]);
const scheduleSafeTxHash = await safeContract.getTransactionHash(
L2_SCROLL_TIMELOCK_ADDR,
0,
timelockScheduleCalldata,
0,
0,
0,
0,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0
);
const sigRawSchedule = await wallet.signMessage(ethers.utils.arrayify(scheduleSafeTxHash));
const sigSchedule = editSig(sigRawSchedule);
await safeContract.checkNSignatures(scheduleSafeTxHash, ethers.utils.arrayify("0x00"), sigSchedule, 1);
await safeContract
.connect(wallet)
.execTransaction(
L2_SCROLL_TIMELOCK_ADDR,
0,
timelockScheduleCalldata,
0,
0,
0,
0,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
sigSchedule,
{ gasLimit: 1000000 }
);
console.log("scheduled");
await timelockContract
.connect(wallet)
.execute(L2_FORWARDER_ADDR, 0, forwarderCalldata, ethers.constants.HashZero, ethers.constants.HashZero, {
gasLimit: 1000000,
});
}
// add 4 to the v byte at the end of the signature
function editSig(sig: string) {
const v = parseInt(sig.slice(-2), 16);
const newV = v + 4;
const newSig = sig.slice(0, -2) + newV.toString(16);
return newSig;
}
main();

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Safe} from "safe-contracts/Safe.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {Forwarder} from "../../src/misc/Forwarder.sol";
contract DeployL1AdminContracts is Script {
uint256 L1_DEPLOYER_PRIVATE_KEY = vm.envUint("L1_DEPLOYER_PRIVATE_KEY");
function run() external {
vm.startBroadcast(L1_DEPLOYER_PRIVATE_KEY);
address council_safe = deploySafe();
// deploy timelock with no delay just to have flow between council and scroll admin
address council_timelock = deployTimelockController(council_safe, 0);
logAddress("L1_COUNCIL_SAFE_ADDR", address(council_safe));
logAddress("L1_COUNCIL_TIMELOCK_ADDR", address(council_timelock));
address scroll_safe = deploySafe();
// TODO: get timelock delay from env. for now just use 2 days
address scroll_timelock = deployTimelockController(scroll_safe, 2 days);
logAddress("L1_SCROLL_SAFE_ADDR", address(scroll_safe));
logAddress("L1_SCROLL_TIMELOCK_ADDR", address(scroll_timelock));
address forwarder = deployForwarder(address(council_safe), address(scroll_safe));
logAddress("L1_FORWARDER_ADDR", address(forwarder));
vm.stopBroadcast();
}
function deployForwarder(address admin, address superAdmin) internal returns (address) {
Forwarder forwarder = new Forwarder(admin, superAdmin);
return address(forwarder);
}
function deploySafe() internal returns (address) {
address owner = vm.addr(L1_DEPLOYER_PRIVATE_KEY);
// TODO: get safe signers from env
Safe safe = new Safe();
address[] memory owners = new address[](1);
owners[0] = owner;
// deployer 1/1. no gas refunds for now
safe.setup(owners, 1, address(0), new bytes(0), address(0), address(0), 0, payable(address(0)));
return address(safe);
}
function deployTimelockController(address safe, uint256 delay) internal returns (address) {
address deployer = vm.addr(L1_DEPLOYER_PRIVATE_KEY);
address[] memory proposers = new address[](1);
proposers[0] = safe;
// add SAFE as the only proposer, anyone can execute
address[] memory executors = new address[](1);
executors[0] = deployer;
TimelockController timelock = new TimelockController(delay, proposers, executors);
bytes32 TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");
// make safe admin of timelock, then revoke deployer's rights
timelock.grantRole(TIMELOCK_ADMIN_ROLE, address(safe));
timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer);
return address(timelock);
}
function logAddress(string memory name, address addr) internal view {
console.log(string(abi.encodePacked(name, "=", vm.toString(address(addr)))));
}
}

View File

@@ -4,8 +4,8 @@ pragma solidity ^0.8.10;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {L1CustomERC20Gateway} from "../../src/L1/gateways/L1CustomERC20Gateway.sol";
import {L1ERC1155Gateway} from "../../src/L1/gateways/L1ERC1155Gateway.sol";
@@ -22,6 +22,7 @@ import {L2GasPriceOracle} from "../../src/L1/rollup/L2GasPriceOracle.sol";
import {ScrollChain} from "../../src/L1/rollup/ScrollChain.sol";
import {Whitelist} from "../../src/L2/predeploys/Whitelist.sol";
contract DeployL1BridgeContracts is Script {
uint256 L1_DEPLOYER_PRIVATE_KEY = vm.envUint("L1_DEPLOYER_PRIVATE_KEY");
@@ -30,14 +31,14 @@ contract DeployL1BridgeContracts is Script {
address L1_WETH_ADDR = vm.envAddress("L1_WETH_ADDR");
address L2_WETH_ADDR = vm.envAddress("L2_WETH_ADDR");
ProxyAdmin proxyAdmin;
// scroll admin (timelocked) or security council
address FORWARDER = vm.envAddress("L1_FORWARDER");
function run() external {
vm.startBroadcast(L1_DEPLOYER_PRIVATE_KEY);
// note: the RollupVerifier library is deployed implicitly
deployProxyAdmin();
deployL1Whitelist();
deployL1MessageQueue();
deployL2GasPriceOracle();
@@ -55,12 +56,6 @@ contract DeployL1BridgeContracts is Script {
vm.stopBroadcast();
}
function deployProxyAdmin() internal {
proxyAdmin = new ProxyAdmin();
logAddress("L1_PROXY_ADMIN_ADDR", address(proxyAdmin));
}
function deployL1Whitelist() internal {
address owner = vm.addr(L1_DEPLOYER_PRIVATE_KEY);
Whitelist whitelist = new Whitelist(owner);
@@ -72,7 +67,7 @@ contract DeployL1BridgeContracts is Script {
ScrollChain impl = new ScrollChain(CHAIN_ID_L2);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -84,7 +79,7 @@ contract DeployL1BridgeContracts is Script {
L1MessageQueue impl = new L1MessageQueue();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
logAddress("L1_MESSAGE_QUEUE_IMPLEMENTATION_ADDR", address(impl));
@@ -95,7 +90,7 @@ contract DeployL1BridgeContracts is Script {
L2GasPriceOracle impl = new L2GasPriceOracle();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
logAddress("L2_GAS_PRICE_ORACLE_IMPLEMENTATION_ADDR", address(impl));
@@ -106,7 +101,7 @@ contract DeployL1BridgeContracts is Script {
L1StandardERC20Gateway impl = new L1StandardERC20Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -118,7 +113,7 @@ contract DeployL1BridgeContracts is Script {
L1ETHGateway impl = new L1ETHGateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -130,7 +125,7 @@ contract DeployL1BridgeContracts is Script {
L1WETHGateway impl = new L1WETHGateway(L1_WETH_ADDR, L2_WETH_ADDR);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -142,7 +137,7 @@ contract DeployL1BridgeContracts is Script {
L1GatewayRouter impl = new L1GatewayRouter();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -154,7 +149,7 @@ contract DeployL1BridgeContracts is Script {
L1ScrollMessenger impl = new L1ScrollMessenger();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -166,7 +161,7 @@ contract DeployL1BridgeContracts is Script {
EnforcedTxGateway impl = new EnforcedTxGateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -178,7 +173,7 @@ contract DeployL1BridgeContracts is Script {
L1CustomERC20Gateway impl = new L1CustomERC20Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -190,7 +185,7 @@ contract DeployL1BridgeContracts is Script {
L1ERC721Gateway impl = new L1ERC721Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -202,7 +197,7 @@ contract DeployL1BridgeContracts is Script {
L1ERC1155Gateway impl = new L1ERC1155Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);

View File

@@ -0,0 +1,114 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Safe} from "safe-contracts/Safe.sol";
import {SafeProxy} from "safe-contracts/proxies/SafeProxy.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {Forwarder} from "../../src/misc/Forwarder.sol";
import {MockTarget} from "../../src/mocks/MockTarget.sol";
interface ISafe {
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external;
}
contract DeployL2AdminContracts is Script {
uint256 L2_DEPLOYER_PRIVATE_KEY = vm.envUint("L2_DEPLOYER_PRIVATE_KEY");
function run() external {
vm.startBroadcast(L2_DEPLOYER_PRIVATE_KEY);
address council_safe = deploySafe();
// deploy timelock with no delay, just to keep council and scroll admin flows be parallel
address council_timelock = deployTimelockController(council_safe, 0);
logAddress("L2_COUNCIL_SAFE_ADDR", address(council_safe));
logAddress("L2_COUNCIL_TIMELOCK_ADDR", address(council_timelock));
address scroll_safe = deploySafe();
// TODO: get timelock delay from env. for now just use 0
address scroll_timelock = deployTimelockController(scroll_safe, 0);
logAddress("L2_SCROLL_SAFE_ADDR", address(scroll_safe));
logAddress("L2_SCROLL_TIMELOCK_ADDR", address(scroll_timelock));
address forwarder = deployForwarder(address(council_timelock), address(scroll_timelock));
logAddress("L1_FORWARDER_ADDR", address(forwarder));
MockTarget target = new MockTarget();
logAddress("L2_TARGET_ADDR", address(target));
vm.stopBroadcast();
}
function deployForwarder(address admin, address superAdmin) internal returns (address) {
Forwarder forwarder = new Forwarder(admin, superAdmin);
return address(forwarder);
}
function deploySafe() internal returns (address) {
address owner = vm.addr(L2_DEPLOYER_PRIVATE_KEY);
// TODO: get safe signers from env
Safe safe = new Safe();
SafeProxy proxy = new SafeProxy(address(safe));
address[] memory owners = new address[](1);
owners[0] = owner;
// deployer 1/1. no gas refunds for now
ISafe(address(proxy)).setup(
owners,
1,
address(0),
new bytes(0),
address(0),
address(0),
0,
payable(address(0))
);
return address(proxy);
}
function deployTimelockController(address safe, uint256 delay) internal returns (address) {
address deployer = vm.addr(L2_DEPLOYER_PRIVATE_KEY);
address[] memory proposers = new address[](1);
proposers[0] = safe;
address[] memory executors = new address[](1);
executors[0] = address(0);
// add SAFE as the only proposer, anyone can execute
TimelockController timelock = new TimelockController(delay, proposers, executors);
bytes32 TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");
// make safe admin of timelock, then revoke deployer's rights
timelock.grantRole(TIMELOCK_ADMIN_ROLE, address(safe));
timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer);
return address(timelock);
}
function logBytes32(string memory name, bytes32 value) internal view {
console.log(string(abi.encodePacked(name, "=", vm.toString(bytes32(value)))));
}
function logUint(string memory name, uint256 value) internal view {
console.log(string(abi.encodePacked(name, "=", vm.toString(uint256(value)))));
}
function logAddress(string memory name, address addr) internal view {
console.log(string(abi.encodePacked(name, "=", vm.toString(address(addr)))));
}
}

View File

@@ -4,7 +4,6 @@ pragma solidity ^0.8.10;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {L2CustomERC20Gateway} from "../../src/L2/gateways/L2CustomERC20Gateway.sol";
@@ -30,10 +29,12 @@ contract DeployL2BridgeContracts is Script {
address L1_WETH_ADDR = vm.envAddress("L1_WETH_ADDR");
address L2_WETH_ADDR = vm.envAddress("L2_WETH_ADDR");
// scroll admin (timelocked) or security council
address FORWARDER = vm.envAddress("L2_FORWARDER");
L1GasPriceOracle oracle;
L1BlockContainer container;
L2MessageQueue queue;
ProxyAdmin proxyAdmin;
// predeploy contracts
address L1_BLOCK_CONTAINER_PREDEPLOY_ADDR = vm.envOr("L1_BLOCK_CONTAINER_PREDEPLOY_ADDR", address(0));
@@ -53,7 +54,6 @@ contract DeployL2BridgeContracts is Script {
deployL2Whitelist();
// upgradable
deployProxyAdmin();
deployL2ScrollMessenger();
deployL2ETHGateway();
deployL2WETHGateway();
@@ -130,17 +130,11 @@ contract DeployL2BridgeContracts is Script {
logAddress("L2_WHITELIST_ADDR", address(whitelist));
}
function deployProxyAdmin() internal {
proxyAdmin = new ProxyAdmin();
logAddress("L2_PROXY_ADMIN_ADDR", address(proxyAdmin));
}
function deployL2ScrollMessenger() internal {
L2ScrollMessenger impl = new L2ScrollMessenger(address(container), address(oracle), address(queue));
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -152,7 +146,7 @@ contract DeployL2BridgeContracts is Script {
L2StandardERC20Gateway impl = new L2StandardERC20Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -164,7 +158,7 @@ contract DeployL2BridgeContracts is Script {
L2ETHGateway impl = new L2ETHGateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -176,7 +170,7 @@ contract DeployL2BridgeContracts is Script {
L2WETHGateway impl = new L2WETHGateway(L2_WETH_ADDR, L1_WETH_ADDR);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -188,7 +182,7 @@ contract DeployL2BridgeContracts is Script {
L2GatewayRouter impl = new L2GatewayRouter();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -208,7 +202,7 @@ contract DeployL2BridgeContracts is Script {
L2CustomERC20Gateway impl = new L2CustomERC20Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -220,7 +214,7 @@ contract DeployL2BridgeContracts is Script {
L2ERC721Gateway impl = new L2ERC721Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);
@@ -232,7 +226,7 @@ contract DeployL2BridgeContracts is Script {
L2ERC1155Gateway impl = new L2ERC1155Gateway();
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(impl),
address(proxyAdmin),
FORWARDER,
new bytes(0)
);

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Forwarder {
address public admin;
address public superAdmin;
event Forwarded(address indexed target, uint256 value, bytes data);
event SetAdmin(address indexed admin);
event SetSuperAdmin(address indexed superAdmin);
constructor(address _admin, address _superAdmin) {
admin = _admin;
superAdmin = _superAdmin;
}
function setAdmin(address _admin) public {
require(msg.sender == superAdmin, "only superAdmin");
admin = _admin;
emit SetAdmin(_admin);
}
function setSuperAdmin(address _superAdmin) public {
require(msg.sender == superAdmin, "only superAdmin");
superAdmin = _superAdmin;
emit SetSuperAdmin(_superAdmin);
}
function forward(address _target, bytes memory _data) public payable {
require(msg.sender == superAdmin || msg.sender == admin, "only admin or superAdmin");
(bool success, ) = _target.call{value: msg.value}(_data);
// bubble up revert reason
if (!success) {
assembly {
let ptr := mload(0x40)
let size := returndatasize()
returndatacopy(ptr, 0, size)
revert(ptr, size)
}
}
emit Forwarded(_target, msg.value, _data);
}
}

View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MockTarget {
event ABC(uint256);
function err() pure external {
revert("test error");
}
function succeed() external {
emit ABC(1);
}
}

View File

@@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
import {WETH} from "solmate/tokens/WETH.sol";
import {Forwarder} from "../misc/Forwarder.sol";
import {MockTarget} from "../mocks/MockTarget.sol";
import {IL1ScrollMessenger, L1ScrollMessenger} from "../L1/L1ScrollMessenger.sol";
contract ForwarderTest is DSTestPlus {
MockTarget public target;
Forwarder public forwarder;
L1ScrollMessenger internal l1Messenger;
address public admin = address(2);
address public superAdmin = address(3);
function setUp() public {
target = new MockTarget();
forwarder = new Forwarder(admin, superAdmin);
l1Messenger = new L1ScrollMessenger();
l1Messenger.initialize(address(0), address(0), address(0), address(0));
l1Messenger.transferOwnership(address(forwarder));
}
function testAdminFail() external {
hevm.expectRevert("only admin or superAdmin");
forwarder.forward(address(l1Messenger),hex"00");
hevm.expectRevert("only superAdmin");
forwarder.setAdmin(address(0));
hevm.expectRevert("only superAdmin");
forwarder.setSuperAdmin(address(0));
}
function testAdmin() external {
// cast calldata "transferOwnership(address)" 0x0000000000000000000000000000000000000005
// 0xf2fde38b0000000000000000000000000000000000000000000000000000000000000005
hevm.startPrank(admin);
forwarder.forward(address(l1Messenger), hex"f2fde38b0000000000000000000000000000000000000000000000000000000000000006");
assertEq(address(6), l1Messenger.owner());
hevm.stopPrank();
}
function testForwardSuperAdmin() external {
hevm.startPrank(superAdmin);
forwarder.forward(address(l1Messenger), hex"f2fde38b0000000000000000000000000000000000000000000000000000000000000006");
assertEq(address(6), l1Messenger.owner());
forwarder.setAdmin(address(0));
assertEq(forwarder.admin(), address(0));
forwarder.setSuperAdmin(address(0));
assertEq(forwarder.superAdmin(), address(0));
}
function testNestedRevert() external {
hevm.startPrank(superAdmin);
hevm.expectRevert("test error");
forwarder.forward(address(target), hex"38df7677");
}
}

View File

@@ -0,0 +1,212 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
import "forge-std/Vm.sol";
// import {Vm, VmSafe} from "./Vm.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
import {Safe} from "safe-contracts/Safe.sol";
import {SafeProxy} from "safe-contracts/proxies/SafeProxy.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
import {Forwarder} from "../../src/misc/Forwarder.sol";
import {MockTarget} from "../../src/mocks/MockTarget.sol";
interface ISafe {
// enum
enum Operation {
Call,
DelegateCall
}
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external;
function execTransaction(
address to,
uint256 value,
bytes calldata data,
Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures
) external returns (bool success);
function checkNSignatures(
bytes32 dataHash,
bytes memory data,
bytes memory signatures,
uint256 requiredSignatures
) external;
}
// scratchpad
contract Temp is DSTestPlus {
address scroll_safe;
// function setUp() external {
// hevm.prank(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
// address council_safe = deploySafe();
// // deploy timelock with no delay, just to keep council and scroll admin flows be parallel
// address council_timelock = deployTimelockController(council_safe, 0);
// // logAddress("L2_COUNCIL_SAFE_ADDR", address(council_safe));
// // logAddress("L2_COUNCIL_TIMELOCK_ADDR", address(council_timelock));
// address scroll_safe = deploySafe();
// // TODO: get timelock delay from env. for now just use 0
// address scroll_timelock = deployTimelockController(scroll_safe, 0);
// // logAddress("L2_SCROLL_SAFE_ADDR", address(scroll_safe));
// // logAddress("L2_SCROLL_TIMELOCK_ADDR", address(scroll_timelock));
// address forwarder = deployForwarder(address(council_safe), address(scroll_safe));
// // logAddress("L1_FORWARDER_ADDR", address(forwarder));
// MockTarget target = new MockTarget();
// // logAddress("L2_TARGET_ADDR", address(target));
// // vm.stopBroadcast();
// }
function testEcrecover() external {
bytes32 dataHash = 0xb453bd4e271eed985cbab8231da609c4ce0a9cf1f763b6c1594e76315510e0f1;
// (uint8 v, bytes32 r, bytes32 s) = signatureSplit(
// hex"078461ca16494711508b8602c1ea3ef515e5bfe11d67fc76e45b9217d42059f57abdde7cb9bf83b094991e2b6e61fd8b1146de575fd12080d65eaedd2e0c74da1c",
// 0
// );
bytes
memory signatures = hex"078461ca16494711508b8602c1ea3ef515e5bfe11d67fc76e45b9217d42059f57abdde7cb9bf83b094991e2b6e61fd8b1146de575fd12080d65eaedd2e0c74da1c";
uint256 requiredSignatures = 1;
uint8 v;
bytes32 r;
bytes32 s;
uint256 i;
for (i = 0; i < requiredSignatures; i++) {
(v, r, s) = signatureSplit(signatures, i);
emit log_uint(v);
emit log_bytes32(r);
emit log_bytes32(s);
address currentOwner = ecrecover(
keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)),
v,
r,
s
);
assertEq(address(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf), currentOwner);
}
}
function testEcrecover1() external {
bytes
memory sig = hex"078461ca16494711508b8602c1ea3ef515e5bfe11d67fc76e45b9217d42059f57abdde7cb9bf83b094991e2b6e61fd8b1146de575fd12080d65eaedd2e0c74da1c";
uint8 v;
bytes32 r;
bytes32 s;
(v, r, s) = signatureSplit(sig, 0);
emit log_uint(v);
emit log_bytes32(r);
emit log_bytes32(s);
require(r == 0x078461ca16494711508b8602c1ea3ef515e5bfe11d67fc76e45b9217d42059f5, "r");
require(s == 0x7abdde7cb9bf83b094991e2b6e61fd8b1146de575fd12080d65eaedd2e0c74da, "s");
require(v == 28, "v");
}
function testSigVerify() external {
address currentOwner = ecrecover(
keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
bytes32(0xb453bd4e271eed985cbab8231da609c4ce0a9cf1f763b6c1594e76315510e0f1)
)
),
28,
0x078461ca16494711508b8602c1ea3ef515e5bfe11d67fc76e45b9217d42059f5,
0x7abdde7cb9bf83b094991e2b6e61fd8b1146de575fd12080d65eaedd2e0c74da
);
require(currentOwner == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "SIG FAIL ABC");
}
function signatureSplit(bytes memory signatures, uint256 pos)
public
returns (
uint8 v,
bytes32 r,
bytes32 s
)
{
// solhint-disable-next-line no-inline-assembly
assembly {
let signaturePos := mul(0x41, pos)
r := mload(add(signatures, add(signaturePos, 0x20)))
s := mload(add(signatures, add(signaturePos, 0x40)))
/**
* Here we are loading the last 32 bytes, including 31 bytes
* of 's'. There is no 'mload8' to do this.
* 'byte' is not working due to the Solidity parser, so lets
* use the second best option, 'and'
*/
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
}
}
function deployForwarder(address admin, address superAdmin) internal returns (address) {
Forwarder forwarder = new Forwarder(admin, superAdmin);
return address(forwarder);
}
function deploySafe() internal returns (address) {
address owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
// TODO: get safe signers from env
Safe safe = new Safe();
SafeProxy proxy = new SafeProxy(address(safe));
address[] memory owners = new address[](1);
owners[0] = owner;
// deployer 1/1. no gas refunds for now
ISafe(address(proxy)).setup(
owners,
1,
address(0),
new bytes(0),
address(0),
address(0),
0,
payable(address(0))
);
return address(proxy);
}
function deployTimelockController(address safe, uint256 delay) internal returns (address) {
address deployer = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
address[] memory proposers = new address[](1);
proposers[0] = safe;
// add SAFE as the only proposer, anyone can execute
TimelockController timelock = new TimelockController(delay, proposers, new address[](0));
bytes32 TIMELOCK_ADMIN_ROLE = keccak256("TIMELOCK_ADMIN_ROLE");
// make safe admin of timelock, then revoke deployer's rights
timelock.grantRole(TIMELOCK_ADMIN_ROLE, address(safe));
timelock.revokeRole(TIMELOCK_ADMIN_ROLE, deployer);
return address(timelock);
}
}