From 38d2d231ee200d318af61d5df9731712189743ef Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Tue, 11 Mar 2025 16:08:46 -0700 Subject: [PATCH 1/5] add deploy script --- .env.example | 11 ----------- scripts/Deploy.s.sol | 45 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) delete mode 100644 .env.example create mode 100644 scripts/Deploy.s.sol diff --git a/.env.example b/.env.example deleted file mode 100644 index 319e1ff5..00000000 --- a/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -# Private key of the EOA to be upgraded to a smart contract wallet -EOA_PRIVATE_KEY= - -# Private key of the account that will deploy contracts and perform the upgrade -DEPLOYER_PRIVATE_KEY= - -# Private key of the new owner to be added to the smart wallet -NEW_OWNER_PRIVATE_KEY= - -# Address of the deployed proxy template on Odyssey (output from UpgradeEOA.s.sol) -PROXY_TEMPLATE_ADDRESS_ODYSSEY= diff --git a/scripts/Deploy.s.sol b/scripts/Deploy.s.sol new file mode 100644 index 00000000..23de9f2b --- /dev/null +++ b/scripts/Deploy.s.sol @@ -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 dev --sender $SENDER --rpc-url $BASE_SEPOLIA_RPC --verify --verifier-url $BASE_SEPOLIA_BLOCKSCOUT_API --etherscan-api-key $BASESCAN_API_KEY --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)); + } +} From 00832b0e4b496db4cb9b5ebf634f1f418619a239 Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Tue, 11 Mar 2025 16:38:58 -0700 Subject: [PATCH 2/5] update to use with odyssey --- scripts/Deploy.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Deploy.s.sol b/scripts/Deploy.s.sol index 23de9f2b..e804754c 100644 --- a/scripts/Deploy.s.sol +++ b/scripts/Deploy.s.sol @@ -15,7 +15,7 @@ import {CoinbaseSmartWalletValidator} from "../src/validators/CoinbaseSmartWalle * @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 dev --sender $SENDER --rpc-url $BASE_SEPOLIA_RPC --verify --verifier-url $BASE_SEPOLIA_BLOCKSCOUT_API --etherscan-api-key $BASESCAN_API_KEY --broadcast -vvvv + * 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 { From fa3246a8ad5f09cc10408471b41682e96b8a5d97 Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Sat, 15 Mar 2025 06:28:10 -0700 Subject: [PATCH 3/5] changes used in developing demo app --- scripts/DeployMocks.s.sol | 29 +++++++++++++++++++++++++++++ test/mocks/MockImplementation.sol | 5 +++-- test/mocks/MockValidator.sol | 2 +- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 scripts/DeployMocks.s.sol diff --git a/scripts/DeployMocks.s.sol b/scripts/DeployMocks.s.sol new file mode 100644 index 00000000..fbdef7e6 --- /dev/null +++ b/scripts/DeployMocks.s.sol @@ -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)); + } +} diff --git a/test/mocks/MockImplementation.sol b/test/mocks/MockImplementation.sol index 0a156793..59530cfa 100644 --- a/test/mocks/MockImplementation.sol +++ b/test/mocks/MockImplementation.sol @@ -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; @@ -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 diff --git a/test/mocks/MockValidator.sol b/test/mocks/MockValidator.sol index cf1415ae..6b1e863b 100644 --- a/test/mocks/MockValidator.sol +++ b/test/mocks/MockValidator.sol @@ -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; } From bbd04df49657178a96aeaeded147e12958378aa5 Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Sat, 15 Mar 2025 06:45:38 -0700 Subject: [PATCH 4/5] =?UTF-8?q?storage=20eraser=20=F0=9F=98=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MultiOwnableStorageEraser.sol | 18 ++++ test/MultiOwnableStorageEraser.t.sol | 139 +++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/MultiOwnableStorageEraser.sol create mode 100644 test/MultiOwnableStorageEraser.t.sol diff --git a/src/MultiOwnableStorageEraser.sol b/src/MultiOwnableStorageEraser.sol new file mode 100644 index 00000000..f8320ed7 --- /dev/null +++ b/src/MultiOwnableStorageEraser.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @dev Malicious contract that erases critical storage slots in MultiOwnable +contract MultiOwnableStorageEraser { + // 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) + } + } +} \ No newline at end of file diff --git a/test/MultiOwnableStorageEraser.t.sol b/test/MultiOwnableStorageEraser.t.sol new file mode 100644 index 00000000..9bd16abc --- /dev/null +++ b/test/MultiOwnableStorageEraser.t.sol @@ -0,0 +1,139 @@ +// 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; + + 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)); + + // 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 pure returns (bytes memory) { + bytes[] memory owners = new bytes[](1); + owners[0] = abi.encode(owner); + 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)))); + } +} From 09bc6c797e99912ca0768cdafcb67252e2fdec59 Mon Sep 17 00:00:00 2001 From: Amie Corso Date: Mon, 17 Mar 2025 09:24:19 -0700 Subject: [PATCH 5/5] more tools used in development of demo app --- scripts/DeployPassingValidator.s.sol | 26 ++++++++++++++++++++++++++ scripts/DeployStorageEraser.s.sol | 26 ++++++++++++++++++++++++++ src/MultiOwnableStorageEraser.sol | 13 ++++++++++--- src/validators/PassingValidator.sol | 14 ++++++++++++++ test/MultiOwnableStorageEraser.t.sol | 15 +++++++-------- 5 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 scripts/DeployPassingValidator.s.sol create mode 100644 scripts/DeployStorageEraser.s.sol create mode 100644 src/validators/PassingValidator.sol diff --git a/scripts/DeployPassingValidator.s.sol b/scripts/DeployPassingValidator.s.sol new file mode 100644 index 00000000..80a3d5aa --- /dev/null +++ b/scripts/DeployPassingValidator.s.sol @@ -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)); + } +} diff --git a/scripts/DeployStorageEraser.s.sol b/scripts/DeployStorageEraser.s.sol new file mode 100644 index 00000000..291a571a --- /dev/null +++ b/scripts/DeployStorageEraser.s.sol @@ -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)); + } +} diff --git a/src/MultiOwnableStorageEraser.sol b/src/MultiOwnableStorageEraser.sol index f8320ed7..06e77290 100644 --- a/src/MultiOwnableStorageEraser.sol +++ b/src/MultiOwnableStorageEraser.sol @@ -1,10 +1,15 @@ // 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 -contract MultiOwnableStorageEraser { +/// 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 = + bytes32 private constant MULTI_OWNABLE_STORAGE_LOCATION = 0x97e2c6aad4ce5d562ebfaa00db6b9e0fb66ea5d8162ed5b243f51a2e03086f00; function eraseNextOwnerIndexStorage() external { @@ -15,4 +20,6 @@ contract MultiOwnableStorageEraser { sstore(storageSlot, 0) } } -} \ No newline at end of file + + function _authorizeUpgrade(address newImplementation) internal virtual override {} +} diff --git a/src/validators/PassingValidator.sol b/src/validators/PassingValidator.sol new file mode 100644 index 00000000..4308d00b --- /dev/null +++ b/src/validators/PassingValidator.sol @@ -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; + } +} diff --git a/test/MultiOwnableStorageEraser.t.sol b/test/MultiOwnableStorageEraser.t.sol index 9bd16abc..1e3c7739 100644 --- a/test/MultiOwnableStorageEraser.t.sol +++ b/test/MultiOwnableStorageEraser.t.sol @@ -15,6 +15,7 @@ contract MultiOwnableStorageEraserTest is Test { uint256 constant _NEW_OWNER_PRIVATE_KEY = 0xB0B; address payable _newOwner; + address payable _secondOwner; CoinbaseSmartWallet _wallet; CoinbaseSmartWallet _cbswImplementation; @@ -34,6 +35,7 @@ contract MultiOwnableStorageEraserTest is Test { // 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(); @@ -76,17 +78,13 @@ contract MultiOwnableStorageEraserTest is Test { vm.etch(_eoa, proxyCode); // Verify storage was erased - assertEq( - CoinbaseSmartWallet(payable(_eoa)).nextOwnerIndex(), - 0, - "nextOwnerIndex should be erased to 0" - ); + 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); + 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); @@ -108,9 +106,10 @@ contract MultiOwnableStorageEraserTest is Test { _wallet = CoinbaseSmartWallet(payable(_eoa)); } - function _createInitArgs(address owner) internal pure returns (bytes memory) { - bytes[] memory owners = new bytes[](1); + 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); }