I am trying to solve a circular dependency in a "toy" app to test upgradable contracts.

I have a Treasury contract containing ERC20 tokens and an upgradable set of Action contracts. The Action contracts interact with other DeFi building blocks like staking on Yearn, swapping on Uniswap, buying bonds on Olympus etc. but the contract itself does not possess any token. Now, to save on gas I'd like to avoid transferring tokens from the Treasury to the Action contracts and then to the external DeFi protocol so I've come up with a possible solutions to that.

Have a special function in the treasury restricted to action contracts only that calls the action back via a delegate call from the Treasury, thus giving full control over the funds.

A scheme User -> Action -> Treasury -> (delegatecall) Action -> external DeFi contract

inside the Treasury

function relayCall(address to, bytes calldata data) external onlyActions {
    require(isActionContract[to], "Err: can only call an action");
    (success,) = to.delegatecall(data);

inside the Action

function makeCall(...) external {
    bytes memory data = abi.encodeWithSelector(Action.swap.selector, ...);
    ITreasury(treasury).relayCall(address(this), data);

function swap(address token0, address token1, address recipient, uint256 amount) external {

The pros of this approach are that it is compatible with an upgradable Action contract it is quite safe as the recipient is whitelisted and the delegate call scope is fixed in the Action code, the big con is the circular dependency Action -> Treasury -> Action.

2 Answers 2


The delegatecall pattern you describe is quite common and is used by contracts such as Gnosis Safe, Gelato, DSProxy and InstaDapp.

The Gnosis Safe actually has a very similar pattern with modules which can trigger also delegatecalls.

The circular dependency could be easily resolved by splitting the contract into a "Trigger" and the "Action". But personally I also don't think that having the circular dependency causes any issues.

What speaks for a separation is to avoid potential unwanted state changes. E.g. it would make sense to not have any state in the "Action" to avoid that is adjusts "Treasury" state.



Do you have to initiate the action from Action? Why not just remove makeCall, and make the entry point Treasury's relayCall?

  • Imagine the Action contract having user-facing functions aswell, say takeLoan and a function to be called by the treasury, stakeOnYearn. A hacker may encode the call to takeLoan and have it executed via the Treasury using the smart contract tokens.
    – Anelito
    Dec 27, 2021 at 8:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.