A reasonably optimized, extensible smart account.
flowchart LR
u{{User}}
e{Ether Deck Mk2}
u -->|call| run --> e
u -->|call| runBatch --> e
u -->|call| setDispatch --> e
u -->|call| setBatchDispatch --> e
e --> mods
e -->|call| target([Target])
mods -->|delegate| Flash([FlashMod])
mods -->|delegate| RevokeMod([RevokeMod])
mods -->|delegate| TransferMod([TransferMod])
// -- snip --
mapping(bytes4 => address) public dispatch;
address public runner;
// -- snip --
flowchart LR
e{EtherDeckMk2}
e --> dispatch
e --> runner([runner])
dispatch --> lo(["bytes4(0x00000000)"])
dispatch --> mid(["bytes4(0x........)"])
dispatch --> hi(["bytes4(0xFFFFFFFF)"])
The core deck storage layout occupies two storage slots in accordance with solidity's storage layout rules.
Slot zero is occupied by a dispatch
mapping which maps four byte selectors to mod addresses that
are authorized to mutate the deck. all dispatchers must be enabled by the deck's
runner
.
Slot one is occupied by a runner
address, the account authorized to run actions on the deck.
function run(address target, bytes calldata payload) external payable;
The run
function makes an external call from the deck with a designated target, payload, and
value.
Reverts if the caller is not the runner
or if the call fails.
function runBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads
) external payable;
The runBatch
function batches external calls from the deck with designated targets, payloads, and
values.
Reverts if the caller is not the runner
, the number of targets, values, and payloads
are inequal, or if any one of the calls fails.
function setDispatch(bytes4 selector, address target) external;
The setDispatch
function sets a target mod in the dispatch
mapping based on the
given selector.
Logs the DispatchSet
event.
Reverts if the caller is not the runner
.
function setDispatchBatch(bytes4[] calldata selectors, address[] calldata targets) external;
The setDispatchBatch
function sets an array of target mods in the dispatch
mapping
based on the provided selector array.
Logs the DispatchSet
event.
Reverts if the caller is not the runner
.
fallback() external payable;
The fallback
function loads the mod address from the dispatch
mapping.
If a mod address is set for the selector, the deck delegate calls to the mod, forwarding all calldata, then bubbling up the returndata to the caller, either reverting or returning, depending on the status of the mod delegate call.
If no mod address is set for the given selector, the deck returns the selector.
Returning the selector when no address is set for the given selector allows the deck to receive tokens whose standards force the receiver to respond to transfer callbacks with the callback selector. Since the deck can make arbitrary contract calls, all future tokens received by the deck may be handled without mods.
Reverts if a mod target is set for the selector and the target reverts.
event DispatchSet(bytes4 indexed selector, address indexed target);
The DispatchSet
event is logged when setDispatch
is called.
flowchart LR
dr{DeckRegistry}
dr --> deployer
dr --> deploy
The DeckRegistry
creates and registers decks. Only existing decks may create decks through the
deck registry.
mapping(address => address) public deployer;
The deployer
function loads the deployer address for a given deck.
function deploy(address runner) external returns (address);
The deploy
function deploys a new deck from another deck.
event Registered(address indexed deployer, address indexed deck)
The Registered
event is logged when deploy
is called.
Mods are contracts that may be delegate called by the deck to extend its functionality. Any contract may be set as a mod for the deck, though serious security considerations must be taken before setting mods to the deck.
BribeMod
: A mod for bribing external parties to run transactions on the runner's behalf.CreatorMod
: A mod for creating contracts.FlashMod
: A mod for issuing ERC-3156 flashloans to external parties.FlatlineMod
: A mod for using a deadman's switch.RevokeMod
: A mod for revoking approvals and operators in batch.TransferMod
: A mod for transferring in batch.StorageMod
: A mod for reading and writing storage slots in batch.TwoStepTransitionMod
: A mod for two-step runner transitions.
flowchart LR
mr{ModRegistry}
mr --> authority
mr --> searchByName
mr --> searchByAddress
mr --> transferAuthority
mr --> register
The ModRegistry
stores mods with name and address lookups.
function authority() external view returns (address);
The authority
function returns the address with authority to mutate the mod factory.
function searchByName(string calldata) external view returns (address);
The searchByName
function returns the address of a given named mod.
function searchByAddress(address) external view returns (string memory);
The searchByAddress
function returns the name of a given mod address.
function transferAuthority(address newAuthority) external;
The transferAuthority
function sets the new authority.
Logs the AuthoritySet
event.
Reverts if the caller is not the authority
.
function register(address modAddress, string calldata modName) external;
The register
function writes to searchByName
and
searchByAddress
for a given mod address and name.
Logs the ModRegistered
event.
Reverts if the caller is not the authority
.
event AuthorityTransferred(address indexed newAuthority);
The AuthorityTransferred
event is logged when transferAuthority
is called.
event ModRegistered(address indexed addr, string name);
The ModRegistered
event is logged on when register
is called.
Invariants:
- no slots collide except when
StorageMod.write
is used - no slot may be written without the explicit consent of the
runner
; consent meaning:- transaction initiation
- signature
- approval
- no tokens may be taken from the deck at the end of the transaction without the explicit consent of the
runner
; consent meaning:- transaction initiation
- signature
- approval
runner
slot is overwritten only:- on
StorageMod.write
call with slot0x01
- to the desginated
receiver
onFlatlineMod.contingency
call after theinterval
has passed sincelastUpdate
- on
TwoStepTransition.acceptRunnerTransition
call by thenewRunner
- on
Assumptions:
runner
will not set a malicious modrunner
is aware of all storage slots passed toStorageMod.write
runner
is aware of all storage slots and indices in each modrunner
will not callFlashMod.setFlashFeeFactor
with a malicious token
Mods have full write access to the deck via delegatecall
.
Per convention, mods that require their own storage must namespace storage using a keccak hash minus one.
uint256 storageSlot = uint256(keccak256("EtherDeckMk2.<storage_name>")) - 1;