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..e804754c --- /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 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)); + } +} 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/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 new file mode 100644 index 00000000..06e77290 --- /dev/null +++ b/src/MultiOwnableStorageEraser.sol @@ -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 {} +} 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 new file mode 100644 index 00000000..1e3c7739 --- /dev/null +++ b/test/MultiOwnableStorageEraser.t.sol @@ -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)))); + } +} 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; }