Skip to content

Commit

Permalink
Proxy-held to proxy-held token swapping
Browse files Browse the repository at this point in the history
  • Loading branch information
area committed Oct 2, 2024
1 parent ca0287a commit 885e93d
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 7 deletions.
2 changes: 1 addition & 1 deletion contracts/bridging/ProxyColony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ contract ProxyColony is DSAuth, Multicall, CallWithGuards, BasicMetaTransaction
// TODO: Stop, or otherwise handle, approve / transferFrom
require(_targets[i] != bridgeAddress, "colony-cannot-target-bridge");
require(_targets[i] != owner, "colony-cannot-target-network");

// TODO: Allowing calling ourselves is okay for now, but as we add functionality might not be?
(bool success, bytes memory returndata) = callWithGuards(_targets[i], _payloads[i]);

// Note that this is not a require because returndata might not be a string, and if we try
Expand Down
1 change: 1 addition & 0 deletions contracts/colony/ColonyAuthority.sol
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ contract ColonyAuthority is CommonAuthority {
addRoleCapability(ROOT_ROLE, "callProxyNetwork(uint256,bytes[])");

addRoleCapability(FUNDING_ROLE, "exchangeTokensViaLiFi(uint256,uint256,uint256,bytes,uint256,address,uint256)");
addRoleCapability(FUNDING_ROLE, "exchangeProxyHeldTokensViaLiFi(uint256,uint256,uint256,bytes,uint256,uint256,address,uint256)");
}

function addRoleCapability(uint8 role, bytes memory sig) private {
Expand Down
65 changes: 65 additions & 0 deletions contracts/colony/ColonyFunding.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ pragma experimental "ABIEncoderV2";
import { ITokenLocking } from "./../tokenLocking/ITokenLocking.sol";
import { ColonyStorage } from "./ColonyStorage.sol";
import { ERC20Extended } from "./../common/ERC20Extended.sol";
import { ERC20 } from "./../../lib/dappsys/erc20.sol";
import { IColonyNetwork } from "./../colonyNetwork/IColonyNetwork.sol";
import { IColony } from "./IColony.sol";
import { DomainTokenReceiver } from "./../common/DomainTokenReceiver.sol";

contract ColonyFunding is
Expand Down Expand Up @@ -243,6 +245,69 @@ contract ColonyFunding is
require(success, "colony-exchange-tokens-failed");
}

function exchangeProxyHeldTokensViaLiFi(
uint256 _permissionDomainId,
uint256 _childSkillIndex,
uint256 _domainId,
bytes memory _txdata,
uint256 _value,
uint256 _chainId,
address _token,
uint256 _amount
) public stoppable authDomain(_permissionDomainId, _childSkillIndex, _domainId) {
// TODO: Colony Network fee

Domain storage d = domains[_domainId];

// Check the domain has enough for what is
if (_token == address(0x0)) {
require(
_value + _amount <= getFundingPotBalance(d.fundingPotId, _chainId, _token),
"colony-insufficient-funds"
);
} else {
require(
_amount <= getFundingPotBalance(d.fundingPotId, _chainId, _token),
"colony-insufficient-funds"
);
require(
_value <= getFundingPotBalance(d.fundingPotId, _chainId, _token),
"colony-insufficient-funds"
);
}

// Deduct the amount from the domain
setFundingPotBalance(
d.fundingPotId,
_chainId,
_token,
getFundingPotBalance(d.fundingPotId, _chainId, _token) - _amount
);

// Deduct the value from the domain
setFundingPotBalance(
d.fundingPotId,
_chainId,
address(0x0),
getFundingPotBalance(d.fundingPotId, _chainId, _token) - _value
);

// Build and send the transaction
if (_token == address(0)) {
revert("not yet implemented");
} else {
address[] memory targets = new address[](2);
targets[0] = _token;
targets[1] = LIFI_ADDRESS;

bytes[] memory payloads = new bytes[](2);
payloads[0] = abi.encodeCall(ERC20.approve, (LIFI_ADDRESS, _amount));
payloads[1] = _txdata;

IColony(address(this)).makeProxyArbitraryTransactions(_chainId, targets, payloads);
}
}

function getNonRewardPotsTotal(address _token) public view returns (uint256) {
return nonRewardPotsTotal[_token];
}
Expand Down
12 changes: 6 additions & 6 deletions contracts/colony/ColonyStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,8 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo
_;
}

modifier authDomain(
uint256 _permissionDomainId,
uint256 _childSkillIndex,
uint256 _childDomainId
) {
modifier authDomain(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _childDomainId)
{
require(domainExists(_permissionDomainId), "ds-auth-permission-domain-does-not-exist");
require(domainExists(_childDomainId), "ds-auth-child-domain-does-not-exist");
require(isAuthorized(msgSender(), _permissionDomainId, msg.sig), "ds-auth-unauthorized");
Expand Down Expand Up @@ -256,7 +253,10 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo

function isAuthorized(address src, uint256 domainId, bytes4 sig) internal view returns (bool) {
return
(src == owner) || DomainRoles(address(authority)).canCall(src, domainId, address(this), sig);
// TODO: Is there a reason we didn't have (src==address(this)?)
(src == owner) ||
(src == address(this)) ||
DomainRoles(address(authority)).canCall(src, domainId, address(this), sig);
}

function isContract(address addr) internal returns (bool) {
Expand Down
21 changes: 21 additions & 0 deletions contracts/colony/IColony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,27 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction, IMultica
uint256 _amount
) external;

/// @notice Exchange funds between two tokens, potentially between chains
/// The tokens being swapped are held by a proxy contract
/// @param _permissionDomainId The domainId in which I have the permission to take this action
/// @param _childSkillIndex The child index in `_permissionDomainId` where we can find `_domainId`
/// @param _domainId Id of the domain
/// @param _txdata Transaction data for the exchange
/// @param _value Value of the transaction
/// @param _chainId The chainId of the token
/// @param _token Address of the token. If the native token is being swapped, can be anything and _amount should be 0.
/// @param _amount Amount of tokens to exchange
function exchangeProxyHeldTokensViaLiFi(
uint256 _permissionDomainId,
uint256 _childSkillIndex,
uint256 _domainId,
bytes memory _txdata,
uint256 _value,
uint256 _chainId,
address _token,
uint256 _amount
) external;

/// @notice Used by the bridge to indicate that funds have been claimed on another chain.
/// @param _chainId Chain id of the chain where the funds were claimed
/// @param _token Address of the token, `0x0` value indicates Ether
Expand Down
19 changes: 19 additions & 0 deletions docs/interfaces/icolony.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,25 @@ Emit a positive skill reputation update. Available only to Root role holders
|_amount|int256|The (positive) amount of reputation to gain


### `exchangeProxyHeldTokensViaLiFi(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId, bytes memory _txdata, uint256 _value, uint256 _chainId, address _token, uint256 _amount)`

Exchange funds between two tokens, potentially between chains The tokens being swapped are held by a proxy contract


**Parameters**

|Name|Type|Description|
|---|---|---|
|_permissionDomainId|uint256|The domainId in which I have the permission to take this action
|_childSkillIndex|uint256|The child index in `_permissionDomainId` where we can find `_domainId`
|_domainId|uint256|Id of the domain
|_txdata|bytes|Transaction data for the exchange
|_value|uint256|Value of the transaction
|_chainId|uint256|The chainId of the token
|_token|address|Address of the token. If the native token is being swapped, can be anything and _amount should be 0.
|_amount|uint256|Amount of tokens to exchange


### `exchangeTokensViaLiFi(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId, bytes memory _txdata, uint256 _value, address _token, uint256 _amount)`

Exchange funds between two tokens, potentially between chains
Expand Down
94 changes: 94 additions & 0 deletions test/cross-chain/cross-chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,100 @@ contract("Cross-chain", (accounts) => {
const balance = await colony.getFundingPotProxyBalance(domain.fundingPotId, foreignChainId, foreignToken.address);
expect(balance.toHexString()).to.equal(ethers.utils.parseEther("50").toHexString());
});

it("can exchange tokens in a domain held by the proxy to different tokens also on the proxy", async () => {
const foreignTokenFactory = new ethers.ContractFactory(MetaTxToken.abi, MetaTxToken.bytecode, ethersForeignSigner);
const foreignToken2 = await foreignTokenFactory.deploy("TT2", "TT2", 18);
await (await foreignToken2.unlock()).wait();
await (await foreignToken.unlock()).wait();

let tx = await foreignToken["mint(address,uint256)"](proxyColony.address, ethers.utils.parseEther("100"));
await tx.wait();
let p = guardianSpy.getPromiseForNextBridgedTransaction();

tx = await proxyColony.claimTokens(foreignToken.address);
await tx.wait();
await p;

// Check bookkeeping on the home chain
const balance = await colony.getFundingPotProxyBalance(1, foreignChainId, foreignToken.address);
expect(balance.toHexString()).to.equal(ethers.utils.parseEther("100").toHexString());

// Move tokens from domain 1 to domain 2
tx = await colony["addDomain(uint256,uint256,uint256)"](1, UINT256_MAX_ETHERS, 1);
await tx.wait();

const domain1 = await colony.getDomain(1);
const domain2 = await colony.getDomain(2);
console.log(domain2);
const fundingPot = await colony.getFundingPot(domain2.fundingPotId);
console.log(fundingPot);

tx = await colony["moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)"](
1,
UINT256_MAX_ETHERS,
1,
UINT256_MAX_ETHERS,
0,
domain1.fundingPotId,
domain2.fundingPotId,
ethers.utils.parseEther("70"),
foreignChainId,
foreignToken.address,
);
await tx.wait();
console.log("moved");
// Exchange tokens
const domain2ReceiverAddress = await homeColonyNetwork.getDomainTokenReceiverAddress(colony.address, 2);

const lifi = new ethers.Contract(LIFI_ADDRESS, LiFiFacetProxyMock.abi, ethersForeignSigner); // Signer doesn't really matter,
// we're just calling encodeFunctionData

const txdata = lifi.interface.encodeFunctionData("swapTokensMock(uint256,address,uint256,address,address,uint256)", [
foreignChainId,
foreignToken.address,
foreignChainId,
foreignToken2.address,
domain2ReceiverAddress,
ethers.utils.parseEther("70"),
]);

p = guardianSpy.getPromiseForNextBridgedTransaction();
tx = await colony.exchangeProxyHeldTokensViaLiFi(1, 0, 2, txdata, 0, foreignChainId, foreignToken.address, ethers.utils.parseEther("70"));
await tx.wait();

const receipt = await p;
const swapEvent = receipt.logs
.filter((e) => e.address === LIFI_ADDRESS)
.map((e) => lifi.interface.parseLog(e))
.filter((e) => e.name === "SwapTokens")[0];
expect(swapEvent).to.not.be.undefined;

// Okay, so we saw the SwapTokens event. Let's do vaguely what it said for the test,
// but in practise this would be the responsibility of whatever entity we've paid to do it
// through LiFi.
await foreignToken2["mint(address,uint256)"](swapEvent.args._toAddress, swapEvent.args._amount); // Implicit 1:1 exchange rate

// Sweep token in to the proxy
p = guardianSpy.getPromiseForNextBridgedTransaction();
tx = await proxyColony.claimTokensForDomain(foreignToken2.address, 2, { gasLimit: 1000000 });
await tx.wait();

// Wait for the sweep to be bridged
await p;

// Check bookkeeping on the home chain
const balance1 = await colony.getFundingPotProxyBalance(1, foreignChainId, foreignToken.address);
const balance2 = await colony.getFundingPotProxyBalance(2, foreignChainId, foreignToken2.address);
expect(balance1.toHexString()).to.equal(ethers.utils.parseEther("30").toHexString());
expect(balance2.toHexString()).to.equal(ethers.utils.parseEther("70").toHexString());

// And check balances of the proxy with the tokens
const balance3 = await foreignToken.balanceOf(proxyColony.address);
const balance4 = await foreignToken2.balanceOf(proxyColony.address);
expect(balance3.toHexString()).to.equal(ethers.utils.parseEther("30").toHexString());
expect(balance4.toHexString()).to.equal(ethers.utils.parseEther("70").toHexString());
});
});

describe("making arbitrary transactions on another chain", async () => {
Expand Down
16 changes: 16 additions & 0 deletions test/deploy-proxy-network-fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Resolver = artifacts.require("Resolver");
const DomainTokenReceiver = artifacts.require("DomainTokenReceiver");

const truffleContract = require("@truffle/contract");
const { setCode } = require("@nomicfoundation/hardhat-network-helpers");
const createXABI = require("../lib/createx/artifacts/src/ICreateX.sol/ICreateX.json");

const { setupEtherRouter } = require("../helpers/upgradable-contracts");
Expand All @@ -15,6 +16,7 @@ const { setupProxyColonyNetwork } = require("../helpers/upgradable-contracts");

const ProxyColonyNetwork = artifacts.require("ProxyColonyNetwork");
const ProxyColony = artifacts.require("ProxyColony");
const LiFiFacetProxyMock = artifacts.require("LiFiFacetProxyMock");

module.exports = async () => {
const accounts = await web3.eth.getAccounts();
Expand Down Expand Up @@ -63,4 +65,18 @@ module.exports = async () => {
const domainTokenReceiverImplementation = await DomainTokenReceiver.new();
await setupEtherRouter("common", "DomainTokenReceiver", { DomainTokenReceiver: domainTokenReceiverImplementation.address }, resolver);
await proxyColonyNetwork.setDomainTokenReceiverResolver(resolver.address);

// Deploy LiFiMock to LiFi address
try {
await setCode("0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", LiFiFacetProxyMock.deployedBytecode);
} catch (error) {
if (error.message.includes("OnlyHardhatNetworkError")) {
await new Promise(function (resolve) {
web3.provider.send(
{ method: "evm_setCode", params: ["0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", LiFiFacetProxyMock.deployedBytecode] },
resolve,
);
});
}
}
};

0 comments on commit 885e93d

Please sign in to comment.