Colony contract

Overview

A Colony involves the deployment and usage of two contracts one for each layers. For example, if the Colony is Optimism/Ethereum there will be two contracts:

  • Colony contract on L2 for the Optimism side

  • Colony contract on L1 for the Ethereum side

Taking an Optimism/Ethereum colony as reference, the scheme of the operation is as follows:

  1. Colony L2 contract receives tokens(ERC20 and/or ETH) according to your business logic

  2. Tokens are moved from Colony L2 contract to Optimism Bridge

  3. Tokens are moved from Optimism Bridge to Colony L1 contract

  4. Colony L1 contract sends tokens to your custom receiver address (wallet/smart contract)

Colony Smart Contract

The AbstractColony contract is an abstract contract that can be inherited to code both the Colony contracts on the two Layers.

AbstractColony is in fact a general purpose contract with several virtual methods that can be overridden to create custom logic depending on the Layer of use.

AbstractColony can handle ETH, Weth and ERC20s on both Layers.

The AbstractColony contract is a LazyInitCapableElement.

Methods

Contract Initialization

Your Colony contract inheriting the AbstractColony can be initialized using the constructor or the lazyInit method provided by LazyInitCapableElement.

It allows the following parameters to be initialized:

  • _wethAddresses -> weth token address to use regarding the layer you're deploying.

  • receiverResolver -> address receiving the tokens from the Colony contract when sendTokens is called (if resolveReceiver() is not overridden with custom logic).

  • executorRewardPercentage -> this is the percentage of the tokens that the sendTokens function caller will receive as a reward.

  • lazyInitResponse -> additional encoded parameters to initialize (look below the following code box).

function _lazyInit(bytes memory data) internal override returns(bytes memory lazyInitResponse) {
    require(receiverResolver == address(0), "init");
    address[] memory _wethAddresses;
    (_wethAddresses, receiverResolver, executorRewardPercentage, lazyInitResponse) = abi.decode(data, (address[], address, uint256, bytes));
    for(uint256 i = 0; i < _wethAddresses.length; i++) {
        address wethAddress = _wethAddresses[i];
        if(wethAddress != address(0)) {
            isWeth[wethAddress] = true;
        }
    }
    lazyInitResponse = _colonyLazyInit(lazyInitResponse);
}

It also provides the virtual method _colonyLazyInit that can be overridden to initialize additional parameters in the contract passed as bytes lazyInitResponse.

function _colonyLazyInit(bytes memory) internal virtual returns(bytes memory) {
}

sendTokens

function sendTokens(address[] memory tokenAddresses, uint32[] memory l1GasArray, address _rewardReceiver) external payable;

It is an external method callable by anyone to send the tokens in the contract to the address that the resolveReceiver() method returns.

It takes as input:

  • tokenAddresses -> Addresses of tokens held by the Colony contract to be transferred. If an address equal to _wethAddresses is passed it is first automatically unwrapped in ETH and then sent.

  • _l1GasArray -> Gas limit required to complete the withdraw on L1. This parameter can be used if the Colony contract sends token to a Bridge which requires the _l1GasArray parameter to bridge tokens (such as the Optimism standard Bridge).

  • _rewardReceiver -> Executor reward receiver.

It provides an executor reward for the msg.sender of the call. For each token transferred by sendToken (each address of tokenAddresses array) the executor reward amount calculated as token amount to be transferred * executor reward percentage is sent to the function caller as a reward.

customize the sendTokens logic

The sendTokens method internally calls the internal virtual _send method:

function sendTokens(address[] memory tokenAddresses, uint32[] memory l1GasArray, address _rewardReceiver) external override {
    ......

    for(uint256 i = 0; i < tokenAddresses.length; i++) {
        address tokenAddress = tokenAddresses[i];
        uint32 l1Gas = l1GasArray.length == 0 ? 0 : i < l1GasArray.length ? l1GasArray[i] : l1GasArray[0];
        if(tokenAddress == address(0) || isWeth[tokenAddress]) {
            bool result = _tryUnwrapWETH(tokenAddress);
            sendETH = sendETH || result;
        } else {
            _send(tokenAddress, to, IERC20(tokenAddress).balanceOf(address(this)), rewardReceiver, l1Gas, "");
        }
    }

    if(sendETH) {
        uint256 balance = address(this).balance;
        if(balance > 0) {
            _send(address(0), to, balance, rewardReceiver, 0, "");
        }
    }
    emit ColonyTransfer();
}

If not overridden, it simply sends the tokens held by the Colony contract to the address that the resolveReceiver() method returns and the executor reward percentage to the address executor of the call.

However being a virtual method it can be overrode to build custom logic. For example in the case of Colony contract L2 the _send call can call the Optimism bridge to move the tokens to L1. Or in case of Colony contract L1 the send call can call send tokens to a specific L1 contract such as an Organization Component smart contract.

resolveReceiver

If not overrode, the resolveReceiver() method always returns the receiverResolver address parameter.

function resolveReceiver() public virtual view returns(address) {
    return receiverResolver;
}

However being a virtual method it can be overridden to get a dynamic receiver or a receiver different from receiverResolver.

Colony Smart Contracts Use Case

EthereansOS Colonies contracts are used to move fees earned on Optimism to Ethereum.

The money flow of the operations is as follows:

  1. Factory on L2 to Colony L2 (via FoF) when a guy does a transaction. ex: withdraw liquidity on Farming Factory

  2. from Colony L2 to Bridge OP (with executor reward)

  3. from OP Bridge to Colony L1. With one transaction per token

  4. from Colony L1 to Splitter contract L1 (with executor reward)

EthereansOS Colony contract on L2

The following reported is the Colony L2 contract used by EthereansOS on Optimism. Consider it a guideline to build your own Colony L2 contract according to your needs.

The purpose of the contract is to receive tokens on Optimism and bridge them by interacting with the standard Optimism bridge.

The Colony contract inherits abstractColony and implements the IL2ERC20Bridge and IL2StandardERC20 interfaces to interact with the standard Optimism bridge.

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;

import "../../../../colonies/impl/AbstractColony.sol";

interface IL2ERC20Bridge {
    function withdrawTo(
        address _l2Token,
        address _to,
        uint256 _amount,
        uint32 _l1Gas,
        bytes calldata _data
    ) external;
}

interface IL2StandardERC20 {
    function l1Token() external view returns (address);
}

contract Colony is AbstractColony {
.......

custom sendTokens logic

The contract provides an override of the virtual _send method (used internally by sendTokens) provided by AbstractColony in order to interact with the Optimism Bridge to bring tokens to L1:

function _send(address token, address to, uint256 amount, uint256 executorReward, address rewardReceiver, uint32 l1Gas, bytes memory payload) internal override {
    if(msg.sender == host) {
        token.safeTransfer(rewardReceiver, amount + executorReward);
    } else {
        token.safeTransfer(rewardReceiver, executorReward);
        _transferToL1(token, to, amount, l1Gas, payload);
    }
}

In this first Colonies version, If there is an host set different from address(0) and if the msg.sender of the sendTokens method is the host (EthereansOS deployer wallet) the tokens are sent to him who will bridge them.

If msg.sender is other than the host address , _transferToL1 is called which interacts with the standard Optimism bridge by bringing the tokens to L1 using the withdrawTo method:

function _transferToL1(address erc20TokenAddress, address to, uint256 value, uint32 l1Gas, bytes memory payload) private {
    if(value == 0) {
        return;
    }
    IL2ERC20Bridge(L2_STANDARD_BRIDGE).withdrawTo(
        erc20TokenAddress == address(0) ? OVM_ETH : erc20TokenAddress,
        to,
        value,
        l1Gas,
        payload
    );
}

The address to passed to withdrawTo is the receiverResolver address previously initialized that is the address of the Colony L1 contract. In this way, tokens are moved from Colony L2 contract on Optimism to Colony L1 contract on Ethereum.

EthereansOS Colony contract on L1

The following reported is the Colony L1 contract used by EthereansOS on Ethereum. Consider it a guideline to build your own Colony L1 contract according to your needs.

The purpose of the contract is to receive tokens on Ethereum from the Optimism Bridge and send them to the EthereansOS Organization (L1).

Custom Initialization

The Colony contract on L1 involves overriding the empty virtual method _colonyLazyInit to initialize an additional parameter decoding the lazyInitResponse parameter:

  • tokenToBurnAddress -> it represents the address of the token that is burned and not transferred when sendTokens is called. In this case it is $OS.

function _colonyLazyInit(bytes memory data) internal override returns(bytes memory) {
    tokenToBurnAddress = abi.decode(data, (address));
    return "";
}

custom sendTokens logic

The contract provides an override of the virtual _send method (used internally by sendTokens) provided by AbstractColony in order to burn $OS.

So all $OS bridged from Optimism to Ethereum via Colonies are burned by Colony L1 contract.

function _send(address token, address to, uint256 amount, uint256 executorReward, address rewardReceiver, uint32, bytes memory) internal override {
    token.safeTransfer(rewardReceiver, executorReward);
    if(token == tokenToBurnAddress) {
        if(amount > 0) {
            ERC20Burnable(tokenToBurnAddress).burn(amount);
        }
    } else {
        token.safeTransfer(to, amount);
    }
}

custom resolveReceiver

The contract provides an override of the virtual resolveReceiver() method provided by AbstractColony setting the receiver address of the tokens (when sendTokens is called) as the TreasurySplitter address or possibly the treasuryManager (if TreasurySplitter is not retrievable) of EthereansOS Organization (L1).

function resolveReceiver() public override view returns(address receiver) {
    IOrganization rootOrganization = IOrganization(receiverResolver);
    ISubDAOsManager subDAOsManager = rootOrganization.subDAOsManager();
    IOrganization subDAO = IOrganization(subDAOsManager.get(Grimoire.SUBDAO_KEY_ETHEREANSOS_V1));
    IOrganization host = address(subDAO) != address(0) ? subDAO : rootOrganization;
    receiver = address(host.treasurySplitterManager());
    receiver = receiver != address(0) ? receiver : address(host.treasuryManager());
}

Last updated