Colony contract
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)
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. 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 whensendTokens
is called (ifresolveReceiver()
is not overridden with custom logic).executorRewardPercentage
-> this is the percentage of the tokens that thesendTokens
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) {
}
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 theColony
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.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.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
.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)
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 {
.......
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.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).
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 whensendTokens
is called. In this case it is $OS.
function _colonyLazyInit(bytes memory data) internal override returns(bytes memory) {
tokenToBurnAddress = abi.decode(data, (address));
return "";
}
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);
}
}
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 modified 7mo ago