Security Model for Depositors / Point Sellers
As a point seller, how do I know that my principal assets, the funds I deposit into my Rumpel wallet, are safe? This page provides the answer.
Security components
Rumpel Wallet is built on top of Safe. Each user wallet is simply a standard Safe with a special Rumpel Module and Rumpel Guard added on. Therefore, our approach to security is largely inherited from Safe, and to understand the scope of an admin’s power, you need only look at these two main added components.
Rumpel Guard – restricts Rumpel user actions to specific calls added onto the allowlist
Rumpel Module – enables the admin to make calls on a user's behalf, unless the calls are on the blocklist
Why is an admin involved at all?
Rumpel Point Tokens are claims on future reward tokens, so when the reward tokens are released, the admin must be able to sweep them from user wallets into the Point Token Vault for pToken redemption. If a user were to withdraw the rewards first, pTokens would go unbacked.
However, there are many assets that we can be confident will never be reward tokens, and therefore, we can permanently enable many actions for users, and disable them for admins. Our contracts are designed with this in mind.
How our security works
The module blocklist and the guard allowlist block and allow calls. Calls are specific function selectors at specific addresses such as USDC.transfer
or RSUSDE.deposit
. While users are the only owners of their 1-of-1 Safe, the Guard allowlist blocks all calls by default, preventing users from making any malicious contract calls. Therefore, only “allowed” calls can be executed by the user.
When a call is added to the module blocklist, an admin can no longer make it on behalf of the user’s Safe. This list is one-way/immutable, meaning once a call is added, it cannot be removed.
🔒 During deployment, enableModule
and disableModule
are added to the blocklist (tx1, tx2), so admins are prevented from changing the module itself to get around the blocklist.
When a call is added to the guard allowlist, users are able to make that call via their Safe. An admin can add and remove these allowed calls, except if they’re set to PERMANENTLY_ON
, in which case an admin can no longer remove them.
To put it simply, in the case of assets:
Blocklist: If an asset is on the blocklist, the admin cannot withdraw it from users
Allowlist: If an asset is on the allowlist, users can withdraw the asset
Permanent Allowlist: When active for an asset, an admin cannot remove it from the allowlist
Example transaction
As defined above, there are three possible actions an admin can take:
add a call to the guard allowlist as
ON
and add a call to the allowlist as
PERMANENTLY_ON
add a call to the module blocklist
Here is a recent batch transaction where each of the three state transitions takes place. Let’s examine it by going through the logs.
Add a call to the guard allowlist as
ON
This first log shows a call added to the guard allowlist with a list state of1
, corresponding toON
. The target is WBTC and the function selector istransfer(address,uint256)
, so this action addsWBTC.transfer
to the allowlist – but an admin has the ability to remove it in the future.

Add a call to the guard allowlist as
PERMANENTLY_ON
This next one shows a call added to the guard allowlist with a list state of2
, corresponding toPERMANENTLY_ON
. The target is RE7LRT and the function selector istransfer(address,uint256)
, so this action addsRE7LRT.transfer
to the allowlist – and an admin has no way of removing it.

Add a call to the module blocklist This last one shows a call added to the module blocklist. The target is RE7LRT and the function selector is
approve(address,uint256)
, so the action addsRE7LRT.approve
to the blocklist. Per the list’s design, an admin cannot remove it, so an admin will never be able to make that call on behalf of a user.

As a result of these 3 changes:
Users can transfer
WBTC
from their SafeUsers can transfer
RE7LRT
from their Safe, and an admin can never disallow itAn admin can never give another address approvals on the
RE7LRT
in a user’s Safe
Audits
The Rumpel Wallet contracts have been audited several times to verify that they behave as outlined above.
We also maintain an ongoing bug bounty program through Sherlock.
Verification
The blocklist & allowlist can easily be verified using events. Here is a Python script that does so:
import os
from dotenv import load_dotenv
from web3 import Web3
# Load environment variables from .env file
load_dotenv()
# Connect to an Ethereum node
w3 = Web3(
Web3.HTTPProvider(
f"<https://eth-mainnet.g.alchemy.com/v2/>{os.environ.get('ALCHEMY_API_KEY', '')}"
)
)
# Contract addresses
RUMPEL_GUARD_ADDRESS = "0x9000FeF2846A5253fD2C6ed5241De0fddb404302"
RUMPEL_MODULE_ADDRESS = "0x28c3498B4956f4aD8d4549ACA8F66260975D361a"
POINT_TOKENIZATION_VAULT_ADDRESS = "0xe47F9Dbbfe98d6930562017ee212C1A1Ae45ba61"
# ABIs
RUMPEL_GUARD_ABI = """[
{"anonymous":false,"inputs":[{"indexed":true,"name":"target","type":"address"},{"indexed":true,"name":"functionSelector","type":"bytes4"},{"indexed":false,"name":"allowListState","type":"uint8"}],"name":"SetCallAllowed","type":"event"}
]"""
RUMPEL_MODULE_ABI = """[
{"anonymous":false,"inputs":[{"indexed":true,"name":"target","type":"address"},{"indexed":true,"name":"functionSelector","type":"bytes4"}],"name":"SetModuleCallBlocked","type":"event"}
]"""
# Create contract instances
rumpel_guard = w3.eth.contract(address=RUMPEL_GUARD_ADDRESS, abi=RUMPEL_GUARD_ABI)
rumpel_module = w3.eth.contract(address=RUMPEL_MODULE_ADDRESS, abi=RUMPEL_MODULE_ABI)
# Token and Protocol addresses
TOKEN_ADDRESSES = {
"0x82f5104b23FF2FA54C2345F821dAc9369e9E0B26": "RSUSDE",
"0x7a4EffD87C2f3C55CA251080b1343b605f327E3a": "RSTETH",
"0xe1B4d34E8754600962Cd944B535180Bd758E6c2e": "AGETH",
"0x9D39A5DE30e57443BfF2A8307A4256c8797A3497": "SUSDE",
"0x4c9EDD5852cd905f086C759E8383e09bff1E68B3": "USDE",
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0": "WSTETH",
"0xBE3cA34D0E877A1Fc889BD5231D65477779AFf4e": "KUSDE",
"0x2DABcea55a12d73191AeCe59F508b191Fb68AdaC": "KWEETH",
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee": "WEETH",
"0x49446A0874197839D15395B908328a74ccc96Bc0": "MSTETH",
"0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a": "RE7LRT",
"0x7F43fDe12A40dE708d908Fb3b9BFB8540d9Ce444": "RE7RWBTC",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599": "WBTC",
"0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84": "stETH",
"0x917ceE801a67f933F2e6b33fC0cD1ED2d5909D88": "weETHs",
"0x0000000000000000000000000000000000000000": "ETH",
}
PROTOCOL_ADDRESSES = {
"0x4095F064B8d3c3548A3bebfd0Bbfd04750E30077": "Morpho Bundler",
"0xF047ab4c75cebf0eB9ed34Ae2c186f3611aEAfa6": "Zircuit Restaking Pool",
"0xC329400492c6ff2438472D4651Ad17389fCb843a": "Symbiotic wstETH Collateral",
"0x19d0D8e6294B7a04a2733FE433444704B791939A": "Symbiotic sUSDe Collateral",
"0x54e44DbB92dBA848ACe27F44c0CB4268981eF1CC": "Karak Vault Supervisor",
"0xAfa904152E04aBFf56701223118Be2832A4449E0": "Karak Delegation Supervisor",
"0x8707f238936c12c309bfc2B9959C35828AcFc512": "Ethena LP Staking",
}
# Known function selectors
KNOWN_SELECTORS = {
"23b872dd": "transferFrom",
"a9059cbb": "transfer",
"095ea7b3": "approve",
"ac9650d8": "multicall",
"f7654176": "depositFor",
"2e1a7d4d": "withdraw",
"b6b55f25": "deposit",
"a0712d68": "mint",
"db006a75": "redeem",
"2e17de78": "unstake",
"70ed1731": "cooldownAssets",
"b88ba9c3": "cooldownShares",
"a694fc3a": "stake",
"5d36b190": "gimmieShares",
"3f2e5fc3": "returnShares",
"5bcee7d8": "depositAndGimmie",
"92dca407": "startWithdraw",
"86e9a1f7": "finishWithdraw",
"441a3e70": "registerWithdrawal",
"610b5925": "enableModule",
"e009cfde": "disableModule",
}
def get_pretty_name(address):
if address == "0x0000000000000000000000000000000000000000":
return "User Safe"
return (
"Point Tokenization Vault"
if address == POINT_TOKENIZATION_VAULT_ADDRESS
else (
TOKEN_ADDRESSES.get(address, f"{address} (Token)")
if address in TOKEN_ADDRESSES
else (
PROTOCOL_ADDRESSES.get(address, f"{address} (Protocol)")
if address in PROTOCOL_ADDRESSES
else address
)
)
)
def get_function_name(selector):
return KNOWN_SELECTORS.get(selector, f"Unknown ({selector})")
def print_allow_list():
print("RumpelGuard Allow List:")
print("=======================")
logs = rumpel_guard.events.SetCallAllowed().get_logs(from_block=0)
allow_list = {}
for log in logs:
target = log.args.target
selector = log.args.functionSelector.hex()
state = log.args.allowListState
if target not in allow_list:
allow_list[target] = {}
allow_list[target][selector] = state
for target, selectors in allow_list.items():
if len(selectors) == 1 and list(selectors.values())[0] == 0:
continue
print(f"\\n{get_pretty_name(target)}:")
for selector, state in selectors.items():
state_str = ["OFF", "ON", "PERMANENTLY_ON"][state]
if state_str != "OFF":
print(f" {get_function_name(selector)}: {state_str}")
def print_block_list():
print("\\nRumpelModule Block List:")
print("========================")
logs = rumpel_module.events.SetModuleCallBlocked().get_logs(from_block=0)
block_list = {}
for log in logs:
target = log.args.target
selector = log.args.functionSelector.hex()
if target not in block_list:
block_list[target] = set()
block_list[target].add(selector)
for target, selectors in block_list.items():
print(f"\\n{get_pretty_name(target)}:")
for selector in selectors:
print(f" {get_function_name(selector)}")
if __name__ == "__main__":
with open("lists.txt", "w") as f:
# Redirect stdout to the file
import sys
original_stdout = sys.stdout
sys.stdout = f
print_allow_list()
print("\\n") # Add more space between lists
print_block_list()
# Restore stdout
sys.stdout = original_stdout
print("Output has been saved to lists.txt")
Last updated
Was this helpful?