Skip to content

Commit 885e93d

Browse files
committed
Proxy-held to proxy-held token swapping
1 parent ca0287a commit 885e93d

File tree

8 files changed

+223
-7
lines changed

8 files changed

+223
-7
lines changed

contracts/bridging/ProxyColony.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ contract ProxyColony is DSAuth, Multicall, CallWithGuards, BasicMetaTransaction
122122
// TODO: Stop, or otherwise handle, approve / transferFrom
123123
require(_targets[i] != bridgeAddress, "colony-cannot-target-bridge");
124124
require(_targets[i] != owner, "colony-cannot-target-network");
125-
125+
// TODO: Allowing calling ourselves is okay for now, but as we add functionality might not be?
126126
(bool success, bytes memory returndata) = callWithGuards(_targets[i], _payloads[i]);
127127

128128
// Note that this is not a require because returndata might not be a string, and if we try

contracts/colony/ColonyAuthority.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ contract ColonyAuthority is CommonAuthority {
142142
addRoleCapability(ROOT_ROLE, "callProxyNetwork(uint256,bytes[])");
143143

144144
addRoleCapability(FUNDING_ROLE, "exchangeTokensViaLiFi(uint256,uint256,uint256,bytes,uint256,address,uint256)");
145+
addRoleCapability(FUNDING_ROLE, "exchangeProxyHeldTokensViaLiFi(uint256,uint256,uint256,bytes,uint256,uint256,address,uint256)");
145146
}
146147

147148
function addRoleCapability(uint8 role, bytes memory sig) private {

contracts/colony/ColonyFunding.sol

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ pragma experimental "ABIEncoderV2";
2222
import { ITokenLocking } from "./../tokenLocking/ITokenLocking.sol";
2323
import { ColonyStorage } from "./ColonyStorage.sol";
2424
import { ERC20Extended } from "./../common/ERC20Extended.sol";
25+
import { ERC20 } from "./../../lib/dappsys/erc20.sol";
2526
import { IColonyNetwork } from "./../colonyNetwork/IColonyNetwork.sol";
27+
import { IColony } from "./IColony.sol";
2628
import { DomainTokenReceiver } from "./../common/DomainTokenReceiver.sol";
2729

2830
contract ColonyFunding is
@@ -243,6 +245,69 @@ contract ColonyFunding is
243245
require(success, "colony-exchange-tokens-failed");
244246
}
245247

248+
function exchangeProxyHeldTokensViaLiFi(
249+
uint256 _permissionDomainId,
250+
uint256 _childSkillIndex,
251+
uint256 _domainId,
252+
bytes memory _txdata,
253+
uint256 _value,
254+
uint256 _chainId,
255+
address _token,
256+
uint256 _amount
257+
) public stoppable authDomain(_permissionDomainId, _childSkillIndex, _domainId) {
258+
// TODO: Colony Network fee
259+
260+
Domain storage d = domains[_domainId];
261+
262+
// Check the domain has enough for what is
263+
if (_token == address(0x0)) {
264+
require(
265+
_value + _amount <= getFundingPotBalance(d.fundingPotId, _chainId, _token),
266+
"colony-insufficient-funds"
267+
);
268+
} else {
269+
require(
270+
_amount <= getFundingPotBalance(d.fundingPotId, _chainId, _token),
271+
"colony-insufficient-funds"
272+
);
273+
require(
274+
_value <= getFundingPotBalance(d.fundingPotId, _chainId, _token),
275+
"colony-insufficient-funds"
276+
);
277+
}
278+
279+
// Deduct the amount from the domain
280+
setFundingPotBalance(
281+
d.fundingPotId,
282+
_chainId,
283+
_token,
284+
getFundingPotBalance(d.fundingPotId, _chainId, _token) - _amount
285+
);
286+
287+
// Deduct the value from the domain
288+
setFundingPotBalance(
289+
d.fundingPotId,
290+
_chainId,
291+
address(0x0),
292+
getFundingPotBalance(d.fundingPotId, _chainId, _token) - _value
293+
);
294+
295+
// Build and send the transaction
296+
if (_token == address(0)) {
297+
revert("not yet implemented");
298+
} else {
299+
address[] memory targets = new address[](2);
300+
targets[0] = _token;
301+
targets[1] = LIFI_ADDRESS;
302+
303+
bytes[] memory payloads = new bytes[](2);
304+
payloads[0] = abi.encodeCall(ERC20.approve, (LIFI_ADDRESS, _amount));
305+
payloads[1] = _txdata;
306+
307+
IColony(address(this)).makeProxyArbitraryTransactions(_chainId, targets, payloads);
308+
}
309+
}
310+
246311
function getNonRewardPotsTotal(address _token) public view returns (uint256) {
247312
return nonRewardPotsTotal[_token];
248313
}

contracts/colony/ColonyStorage.sol

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,8 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo
199199
_;
200200
}
201201

202-
modifier authDomain(
203-
uint256 _permissionDomainId,
204-
uint256 _childSkillIndex,
205-
uint256 _childDomainId
206-
) {
202+
modifier authDomain(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _childDomainId)
203+
{
207204
require(domainExists(_permissionDomainId), "ds-auth-permission-domain-does-not-exist");
208205
require(domainExists(_childDomainId), "ds-auth-child-domain-does-not-exist");
209206
require(isAuthorized(msgSender(), _permissionDomainId, msg.sig), "ds-auth-unauthorized");
@@ -256,7 +253,10 @@ contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, Commo
256253

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

262262
function isContract(address addr) internal returns (bool) {

contracts/colony/IColony.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,27 @@ interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction, IMultica
933933
uint256 _amount
934934
) external;
935935

936+
/// @notice Exchange funds between two tokens, potentially between chains
937+
/// The tokens being swapped are held by a proxy contract
938+
/// @param _permissionDomainId The domainId in which I have the permission to take this action
939+
/// @param _childSkillIndex The child index in `_permissionDomainId` where we can find `_domainId`
940+
/// @param _domainId Id of the domain
941+
/// @param _txdata Transaction data for the exchange
942+
/// @param _value Value of the transaction
943+
/// @param _chainId The chainId of the token
944+
/// @param _token Address of the token. If the native token is being swapped, can be anything and _amount should be 0.
945+
/// @param _amount Amount of tokens to exchange
946+
function exchangeProxyHeldTokensViaLiFi(
947+
uint256 _permissionDomainId,
948+
uint256 _childSkillIndex,
949+
uint256 _domainId,
950+
bytes memory _txdata,
951+
uint256 _value,
952+
uint256 _chainId,
953+
address _token,
954+
uint256 _amount
955+
) external;
956+
936957
/// @notice Used by the bridge to indicate that funds have been claimed on another chain.
937958
/// @param _chainId Chain id of the chain where the funds were claimed
938959
/// @param _token Address of the token, `0x0` value indicates Ether

docs/interfaces/icolony.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,25 @@ Emit a positive skill reputation update. Available only to Root role holders
378378
|_amount|int256|The (positive) amount of reputation to gain
379379

380380

381+
### `exchangeProxyHeldTokensViaLiFi(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId, bytes memory _txdata, uint256 _value, uint256 _chainId, address _token, uint256 _amount)`
382+
383+
Exchange funds between two tokens, potentially between chains The tokens being swapped are held by a proxy contract
384+
385+
386+
**Parameters**
387+
388+
|Name|Type|Description|
389+
|---|---|---|
390+
|_permissionDomainId|uint256|The domainId in which I have the permission to take this action
391+
|_childSkillIndex|uint256|The child index in `_permissionDomainId` where we can find `_domainId`
392+
|_domainId|uint256|Id of the domain
393+
|_txdata|bytes|Transaction data for the exchange
394+
|_value|uint256|Value of the transaction
395+
|_chainId|uint256|The chainId of the token
396+
|_token|address|Address of the token. If the native token is being swapped, can be anything and _amount should be 0.
397+
|_amount|uint256|Amount of tokens to exchange
398+
399+
381400
### `exchangeTokensViaLiFi(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId, bytes memory _txdata, uint256 _value, address _token, uint256 _amount)`
382401

383402
Exchange funds between two tokens, potentially between chains

test/cross-chain/cross-chain.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,100 @@ contract("Cross-chain", (accounts) => {
913913
const balance = await colony.getFundingPotProxyBalance(domain.fundingPotId, foreignChainId, foreignToken.address);
914914
expect(balance.toHexString()).to.equal(ethers.utils.parseEther("50").toHexString());
915915
});
916+
917+
it("can exchange tokens in a domain held by the proxy to different tokens also on the proxy", async () => {
918+
const foreignTokenFactory = new ethers.ContractFactory(MetaTxToken.abi, MetaTxToken.bytecode, ethersForeignSigner);
919+
const foreignToken2 = await foreignTokenFactory.deploy("TT2", "TT2", 18);
920+
await (await foreignToken2.unlock()).wait();
921+
await (await foreignToken.unlock()).wait();
922+
923+
let tx = await foreignToken["mint(address,uint256)"](proxyColony.address, ethers.utils.parseEther("100"));
924+
await tx.wait();
925+
let p = guardianSpy.getPromiseForNextBridgedTransaction();
926+
927+
tx = await proxyColony.claimTokens(foreignToken.address);
928+
await tx.wait();
929+
await p;
930+
931+
// Check bookkeeping on the home chain
932+
const balance = await colony.getFundingPotProxyBalance(1, foreignChainId, foreignToken.address);
933+
expect(balance.toHexString()).to.equal(ethers.utils.parseEther("100").toHexString());
934+
935+
// Move tokens from domain 1 to domain 2
936+
tx = await colony["addDomain(uint256,uint256,uint256)"](1, UINT256_MAX_ETHERS, 1);
937+
await tx.wait();
938+
939+
const domain1 = await colony.getDomain(1);
940+
const domain2 = await colony.getDomain(2);
941+
console.log(domain2);
942+
const fundingPot = await colony.getFundingPot(domain2.fundingPotId);
943+
console.log(fundingPot);
944+
945+
tx = await colony["moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)"](
946+
1,
947+
UINT256_MAX_ETHERS,
948+
1,
949+
UINT256_MAX_ETHERS,
950+
0,
951+
domain1.fundingPotId,
952+
domain2.fundingPotId,
953+
ethers.utils.parseEther("70"),
954+
foreignChainId,
955+
foreignToken.address,
956+
);
957+
await tx.wait();
958+
console.log("moved");
959+
// Exchange tokens
960+
const domain2ReceiverAddress = await homeColonyNetwork.getDomainTokenReceiverAddress(colony.address, 2);
961+
962+
const lifi = new ethers.Contract(LIFI_ADDRESS, LiFiFacetProxyMock.abi, ethersForeignSigner); // Signer doesn't really matter,
963+
// we're just calling encodeFunctionData
964+
965+
const txdata = lifi.interface.encodeFunctionData("swapTokensMock(uint256,address,uint256,address,address,uint256)", [
966+
foreignChainId,
967+
foreignToken.address,
968+
foreignChainId,
969+
foreignToken2.address,
970+
domain2ReceiverAddress,
971+
ethers.utils.parseEther("70"),
972+
]);
973+
974+
p = guardianSpy.getPromiseForNextBridgedTransaction();
975+
tx = await colony.exchangeProxyHeldTokensViaLiFi(1, 0, 2, txdata, 0, foreignChainId, foreignToken.address, ethers.utils.parseEther("70"));
976+
await tx.wait();
977+
978+
const receipt = await p;
979+
const swapEvent = receipt.logs
980+
.filter((e) => e.address === LIFI_ADDRESS)
981+
.map((e) => lifi.interface.parseLog(e))
982+
.filter((e) => e.name === "SwapTokens")[0];
983+
expect(swapEvent).to.not.be.undefined;
984+
985+
// Okay, so we saw the SwapTokens event. Let's do vaguely what it said for the test,
986+
// but in practise this would be the responsibility of whatever entity we've paid to do it
987+
// through LiFi.
988+
await foreignToken2["mint(address,uint256)"](swapEvent.args._toAddress, swapEvent.args._amount); // Implicit 1:1 exchange rate
989+
990+
// Sweep token in to the proxy
991+
p = guardianSpy.getPromiseForNextBridgedTransaction();
992+
tx = await proxyColony.claimTokensForDomain(foreignToken2.address, 2, { gasLimit: 1000000 });
993+
await tx.wait();
994+
995+
// Wait for the sweep to be bridged
996+
await p;
997+
998+
// Check bookkeeping on the home chain
999+
const balance1 = await colony.getFundingPotProxyBalance(1, foreignChainId, foreignToken.address);
1000+
const balance2 = await colony.getFundingPotProxyBalance(2, foreignChainId, foreignToken2.address);
1001+
expect(balance1.toHexString()).to.equal(ethers.utils.parseEther("30").toHexString());
1002+
expect(balance2.toHexString()).to.equal(ethers.utils.parseEther("70").toHexString());
1003+
1004+
// And check balances of the proxy with the tokens
1005+
const balance3 = await foreignToken.balanceOf(proxyColony.address);
1006+
const balance4 = await foreignToken2.balanceOf(proxyColony.address);
1007+
expect(balance3.toHexString()).to.equal(ethers.utils.parseEther("30").toHexString());
1008+
expect(balance4.toHexString()).to.equal(ethers.utils.parseEther("70").toHexString());
1009+
});
9161010
});
9171011

9181012
describe("making arbitrary transactions on another chain", async () => {

test/deploy-proxy-network-fixture.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const Resolver = artifacts.require("Resolver");
66
const DomainTokenReceiver = artifacts.require("DomainTokenReceiver");
77

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

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

1617
const ProxyColonyNetwork = artifacts.require("ProxyColonyNetwork");
1718
const ProxyColony = artifacts.require("ProxyColony");
19+
const LiFiFacetProxyMock = artifacts.require("LiFiFacetProxyMock");
1820

1921
module.exports = async () => {
2022
const accounts = await web3.eth.getAccounts();
@@ -63,4 +65,18 @@ module.exports = async () => {
6365
const domainTokenReceiverImplementation = await DomainTokenReceiver.new();
6466
await setupEtherRouter("common", "DomainTokenReceiver", { DomainTokenReceiver: domainTokenReceiverImplementation.address }, resolver);
6567
await proxyColonyNetwork.setDomainTokenReceiverResolver(resolver.address);
68+
69+
// Deploy LiFiMock to LiFi address
70+
try {
71+
await setCode("0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", LiFiFacetProxyMock.deployedBytecode);
72+
} catch (error) {
73+
if (error.message.includes("OnlyHardhatNetworkError")) {
74+
await new Promise(function (resolve) {
75+
web3.provider.send(
76+
{ method: "evm_setCode", params: ["0x1231DEB6f5749EF6cE6943a275A1D3E7486F4EaE", LiFiFacetProxyMock.deployedBytecode] },
77+
resolve,
78+
);
79+
});
80+
}
81+
}
6682
};

0 commit comments

Comments
 (0)