The Action Core Pattern: Upgradability for Lamden Contracts

Stuart Farmer
Lamden
Published in
4 min readOct 4, 2021

One of the problems with smart contracts is that once you deploy one, it’s very hard to upgrade or change the code to do so. Ethereum developers have come up with their own ‘Proxy’ pattern which essentially forwards transactions to the proper delegate smart contract. If there is a new version of the smart contract, the delegate smart contract address is changed in the proxy.

This has a few downsides:

  1. You must swap the entire smart contract. So if you want to add a small feature to your dapp, you must rewrite and redeploy the entire thing.
  2. No state sharing. Smart contracts on Ethereum have a single state per contract. You have to have explicit setter and getter functions for each portion of the state. For example, if I have a DEX that keeps track of balances but I want to upgrade to a new version, the balances from my old version have to be migrated over or lazily loaded into the new version. This is, in my opinion, sloppy.

Enter the Action Core

The Demon Core nuclear incident. Unrelated, but cool sounding, just like the Action Core.

In Lamden smart contracts, you can pass state variables (Hashes and Variables) into other smart contracts as arguments. When the receiving smart contract reads and writes upon those variables, they are written to the original smart contract storage, not the receiving smart contract’s storage.

This is not a well-known feature, but it is vital to the Action Core pattern.

# Convenience
I = importlib

S = Hash()
actions = Hash()
owner = Variable()

# Policy interface
action_interface = [
I.Func('interact', args=('payload', 'state', 'caller')),
]

@construct
def seed():
owner.set(ctx.caller)

@export
def change_owner(new_owner: str):
assert ctx.caller == owner.get(), 'Only owner can call!'
owner.set(new_owner)

@export
def register_action(action: str, contract: str):
assert ctx.caller == owner.get(), 'Only owner can call!'
assert actions[action] is None, 'Action already registered!'
# Attempt to import the contract to make sure it is already submitted
p = I.import_module(contract)

# Assert ownership is election_house and interface is correct
assert I.owner_of(p) == ctx.this, \
'This contract must control the action contract!'

assert I.enforce_interface(p, action_interface), \
'Action contract does not follow the correct interface!'

actions[action] = contract

@export
def unregister_action(action: str):
assert ctx.caller == owner.get(), 'Only owner can call!'
assert actions[action] is not None, 'Action does not exist!'

actions[action] = None

@export
def interact(action: str, payload: dict):
contract = actions[action]
assert contract is not None, 'Invalid action!'

module = I.import_module(contract)

result = module.interact(payload, S, ctx.caller)
return result

@export
def bulk_interact(action: str, payloads: list):
for payload in payloads:
interact(action, payload)

Above is the Action Core code. Instead of ‘upgrading’ contracts, you add ‘actions’ or features to your application. These actions accept a payload argument which then has all the information the registered contracts need to execute their logic.

Each registered contract must adhere to a standard interface. However, inside that contract, the payload can distinguish from different pieces of logic within that registered smart contract. For example:

# Expects a payload with keys and values such that:
# payload['function'] -> 'add' or 'sub'
# payload['x'] -> number
# payload['y'] -> number
#
# Different logic is run given the value of the 'function' key
@export
def interact(payload: dict, state: Any, caller: str):
if payload['function'] == 'add':
add(payload['x'], payload['y'])
elif payload['function'] == 'sub':
sub(payload['x'], payload['y'])
else:
raise Exception('Invalid function name')
def add(x, y, state):
state['result'] = x + y
def sub(x, y, state):
state['result'] = x - y

This action contract can be attached to the action core and would take care of ‘math’. We would send the following transactions to the action core to make this happen:

1. Submit the action core contract2. Submit the action contract, setting the owner (done on submission) to the action core contract.3. Execute register_action(action='math', contract=<action contract name>)

Voila!

Let’s say we wanted to add more math functions. Our new action contract could be:

# Expects a payload with keys and values such that:
# payload['function'] -> 'add', 'sub', 'mul', 'div'
# payload['x'] -> number
# payload['y'] -> number
#
# Different logic is run given the value of the 'function' key
@export
def interact(payload: dict, state: Any, caller: str):
if payload['function'] == 'add':
add(payload['x'], payload['y'])
elif payload['function'] == 'sub':
sub(payload['x'], payload['y'])
# New code!
elif payload['function'] == 'mul':
mul(payload['x'], payload['y'])
elif payload['function'] == 'sub':
div(payload['x'], payload['y'])
else:
raise Exception('Invalid function name')
def add(x, y, state):
state['result'] = x + y
def sub(x, y, state):
state['result'] = x - y
def mul(x, y, state):
state['result'] = x * y
def div(x, y, state):
state['result'] = x / y

We could then upgrade our action core without affecting the state by simply executing the following:

4. Submit the new action contract.5. Execute unregister_action(action='math')6. Execute register_action(action='math', contract=<new action contract name>

We can also bolt on new features besides ‘math.’ Let’s say we want to add a ‘status’ feature.

# Expects a payload with keys and values such that:
# payload['status'] -> str
#
# Writes to the state at ctx.caller the new status
@export
def interact(payload: dict, state: Any, caller: str):
state[caller] = payload['status']

And to add this to the action core:

7. Submit the status action contract.8. Execute register_action(action='status', contract=<status action contract name>

This is all there is to it! The Action Core Pattern offers an extremely flexible and iterative way to create your dapps that is more akin to traditional software development than the quirks of decentralized programming. Enjoy :)

Complete Action Core contract with unit tests can be found here: https://github.com/Lamden/action_core

--

--