Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connectors v1 #16

Merged
merged 46 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e7b05c4
add withdraw & deposit
ilinzweilin Jul 22, 2022
ff49895
make withdraw function public
ilinzweilin Jul 26, 2022
1ab18a8
add sendMessages to router
ilinzweilin Jul 28, 2022
ddf1023
update deploy script and add test
AStox Jul 31, 2022
40049f6
switch Nomad to XCM router in tests
ilinzweilin Aug 2, 2022
d98626e
add happy case deposit & withdraw tests
ilinzweilin Aug 4, 2022
f32024b
remove unused code
ilinzweilin Aug 4, 2022
ed9515f
add xAppConnectionManager
ilinzweilin Aug 9, 2022
2ed1b5a
add assertions for withdraw happu case
ilinzweilin Aug 9, 2022
b1284e0
add edges cases for transfer route
ilinzweilin Aug 19, 2022
47ec66f
add edge case tests for transferTo
ilinzweilin Aug 19, 2022
abc64b2
fix merge conflicts
ilinzweilin Aug 19, 2022
fd2ecde
remove misleading messages
ilinzweilin Aug 19, 2022
335820c
remove misleading messages
ilinzweilin Aug 19, 2022
d45f17e
update origin address
AStox Aug 25, 2022
3cb96e8
Merge branch 'main' into update-deploy-script
AStox Aug 25, 2022
71d553a
Use correct checksummed origin address
AStox Aug 25, 2022
e1cfc14
Strip out nomad and testing dependencies from XCMRouter. Add ping fun…
AStox Aug 26, 2022
2e47470
Update router
hieronx Aug 29, 2022
8338a3b
fix naming
ilinzweilin Aug 30, 2022
33a8191
Update centrifuge origin address
AStox Sep 29, 2022
1c57fa7
fix updateMember parsing
AStox Oct 5, 2022
65ed9bb
roll back updateMember change
AStox Nov 17, 2022
6593be2
Clean up
hieronx Nov 18, 2022
7da41b5
forge install: memview-sol
hieronx Nov 18, 2022
84c7f39
Fix all the tests
hieronx Nov 18, 2022
8fe2b8b
Merge branch 'update-deploy-script' of https://github.com/centrifuge/…
hieronx Nov 18, 2022
644dbb2
Fix deps
hieronx Nov 18, 2022
7f3870b
Remove yarn steps from workflows
hieronx Nov 18, 2022
827797c
Remove deploy test
hieronx Nov 18, 2022
e2f01b2
Merge branch 'add-withdaw-deposit' of https://github.com/centrifuge/c…
hieronx Nov 18, 2022
a6d8f58
merge main
AStox Jan 6, 2023
c8d6c3b
Remove nomad router's send message logic and tests pertaining to it
AStox Jan 16, 2023
1a287d9
Fix memview-sol branch submodule not found
NunoAlexandre Jan 26, 2023
f744dd2
Drop "== true" blocks
NunoAlexandre Jan 30, 2023
e2832fe
Fix `UpdateMember` encoding & decoding (#32)
NunoAlexandre Jan 30, 2023
2d91780
Fix 9-24: trancheId (16 bytes) docs
NunoAlexandre Jan 30, 2023
afb740c
feat: Add `price` field to `AddTranche` (#33)
NunoAlexandre Jan 31, 2023
2ae621d
Make `UpdateTokenPrice.price` and `Tranche.latestPrice` `u128` (#34)
NunoAlexandre Jan 31, 2023
d1898d8
Use cent-chain AddTranche message
NunoAlexandre Jan 31, 2023
aa41811
Align tests with cent chain (#36)
NunoAlexandre Feb 2, 2023
78edd87
Chore/add tests and optimizations (#38)
AStox Feb 6, 2023
8a20e95
Feat/domain update (#35)
AStox Feb 7, 2023
54dd4bb
Fine-tune transfers encoding, decoding, and tests (#39)
NunoAlexandre Feb 7, 2023
695f1f0
Add TODO and more tests
AStox Feb 8, 2023
0b28d84
Update test/Connector.t.sol
Feb 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
[submodule "lib/memview-sol"]
path = lib/memview-sol
url = https://github.com/summa-tx/memview-sol
branch = v2.1.1
branch = main
42 changes: 30 additions & 12 deletions src/Connector.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ pragma abicoder v2;
import { RestrictedTokenFactoryLike, MemberlistFactoryLike } from "./token/factory.sol";
import { RestrictedTokenLike } from "./token/restricted.sol";
import { MemberlistLike } from "./token/memberlist.sol";
import "forge-std/Test.sol";
import { ConnectorMessages } from "src/Messages.sol";
NunoAlexandre marked this conversation as resolved.
Show resolved Hide resolved

interface RouterLike {
function sendMessage(uint64 poolId, bytes16 trancheId, uint256 amount, address user) external;
}

contract CentrifugeConnector is Test {
contract CentrifugeConnector {

RouterLike public router;
RestrictedTokenFactoryLike public immutable tokenFactory;
Expand All @@ -24,7 +25,7 @@ contract CentrifugeConnector is Test {

struct Tranche {
address token;
uint256 latestPrice; // [ray]
uint128 latestPrice; // [ray]
uint256 lastPriceUpdate;
// TODO: the token name & symbol need to be stored because of the separation between adding and deploying tranches.
// This leads to duplicate storage (also in the ERC20 contract), ideally we should refactor this somehow
Expand Down Expand Up @@ -86,15 +87,16 @@ contract CentrifugeConnector is Test {
emit PoolAdded(poolId);
}

function addTranche(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol)
function addTranche(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol, uint128 price)
public
onlyRouter
{
Pool storage pool = pools[poolId];
require(pool.createdAt > 0, "CentrifugeConnector/invalid-pool");

Tranche storage tranche = tranches[poolId][trancheId];
tranche.latestPrice = 1*10**27;
tranche.latestPrice = price;
tranche.lastPriceUpdate = block.timestamp;
tranche.tokenName = tokenName;
tranche.tokenSymbol = tokenSymbol;

Expand All @@ -111,17 +113,17 @@ contract CentrifugeConnector is Test {

hieronx marked this conversation as resolved.
Show resolved Hide resolved
address memberlist = memberlistFactory.newMemberlist();
RestrictedTokenLike(token).depend("memberlist", memberlist);

MemberlistLike(memberlist).updateMember(address(this), uint(-1)); // required to be able to receive tokens in case of withdrawals
ilinzweilin marked this conversation as resolved.
Show resolved Hide resolved
NunoAlexandre marked this conversation as resolved.
Show resolved Hide resolved
emit TrancheDeployed(poolId, trancheId, token);
}

function updateTokenPrice(
uint64 poolId,
bytes16 trancheId,
uint256 price
uint128 price
) public onlyRouter {
Tranche storage tranche = tranches[poolId][trancheId];
require(tranche.latestPrice > 0, "CentrifugeConnector/invalid-pool-or-tranche");
require(tranche.lastPriceUpdate > 0, "CentrifugeConnector/invalid-pool-or-tranche");
tranche.latestPrice = price;
tranche.lastPriceUpdate = block.timestamp;
}
Expand All @@ -130,24 +132,40 @@ contract CentrifugeConnector is Test {
uint64 poolId,
bytes16 trancheId,
address user,
uint256 validUntil
uint64 validUntil
) public onlyRouter {
Tranche storage tranche = tranches[poolId][trancheId];
require(tranche.latestPrice > 0, "CentrifugeConnector/invalid-pool-or-tranche");
require(tranche.lastPriceUpdate > 0, "CentrifugeConnector/invalid-pool-or-tranche");
RestrictedTokenLike token = RestrictedTokenLike(tranche.token);
MemberlistLike memberlist = MemberlistLike(token.memberlist());
memberlist.updateMember(user, validUntil);
}

function transferTo(
function handleTransfer(
uint64 poolId,
bytes16 trancheId,
address user,
uint256 amount
) public onlyRouter {
RestrictedTokenLike token = RestrictedTokenLike(tranches[poolId][trancheId].token);
require(token.hasMember(user), "CentrifugeConnector/not-a-member");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test for this already, i.e. testing that a transfer to a non-whitelisted address fails? I think this check is actually redundant as the underlying erc20 token already checks that the destination address is a member.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping this ^ @NunoAlexandre @AStox

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minting doesn't check the memberlist. This was the change I had made to restricted.sol, since it seems kind of odd that a permissioned token can mint to any address

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I would keep it like this for now, to not change the existing restricted token implementation. But let's discuss this a bit more and we can still change it in V2 optionally.


token.mint(user, amount);
}


function transfer(
uint64 poolId,
bytes16 trancheId,
address user,
uint256 amount,
ConnectorMessages.Domain domain
) public {
require(domain == ConnectorMessages.Domain.Centrifuge, "CentrifugeConnector/invalid-domain");
RestrictedTokenLike token = RestrictedTokenLike(tranches[poolId][trancheId].token);
require(address(token) != address(0), "CentrifugeConnector/unknown-token");
require(token.balanceOf(user) >= amount, "CentrifugeConnector/insufficient-balance");
require(token.transferFrom(user, address(this), amount), "CentrifugeConnector/token-transfer-failed");
token.burn(address(this), amount);
router.sendMessage(poolId, trancheId, amount, user);
}
}
85 changes: 65 additions & 20 deletions src/Messages.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ library ConnectorMessages {
AddTranche,
UpdateTokenPrice,
UpdateMember,
TransferTo
Transfer
}

enum Domain { Centrifuge, EVM }

function messageType(bytes29 _msg) internal pure returns (Call _call) {
_call = Call(uint8(_msg.indexUint(0, 1)));
}
Expand Down Expand Up @@ -43,26 +45,35 @@ library ConnectorMessages {
*
* 0: call type (uint8 = 1 byte)
* 1-8: poolId (uint64 = 8 bytes)
* 9-25: trancheId (16 bytes)
* 26-154: tokenName (string = 128 bytes)
* 9-24: trancheId (16 bytes)
* 25-154: tokenName (string = 128 bytes)
* 155-187: tokenSymbol (string = 32 bytes)
* 185-200: price (uint128 = 16 bytes)
*/
function formatAddTranche(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol) internal pure returns (bytes memory) {
function formatAddTranche(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol, uint128 price) internal pure returns (bytes memory) {
// TODO(nuno): Now, we encode `tokenName` as a 128-bytearray by first encoding `tokenName`
// to bytes32 and then we encode three empty bytes32's, which sum up to a total of 128 bytes.
// Add support to actually encode `tokennName` fully as a 128 bytes string.
return abi.encodePacked(uint8(Call.AddTranche), poolId, trancheId, stringToBytes32(tokenName), bytes32(""), bytes32(""), bytes32(""), stringToBytes32(tokenSymbol));
// Add support to actually encode `tokenName` fully as a 128 bytes string.
return abi.encodePacked(
uint8(Call.AddTranche),
poolId,
trancheId,
stringToBytes32(tokenName), bytes32(""), bytes32(""), bytes32(""),
stringToBytes32(tokenSymbol),
price
);
}

function isAddTranche(bytes29 _msg) internal pure returns (bool) {
return messageType(_msg) == Call.AddTranche;
}

function parseAddTranche(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol) {
function parseAddTranche(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol, uint128 price) {
poolId = uint64(_msg.indexUint(1, 8));
trancheId = bytes16(_msg.index(9, 16));
tokenName = bytes32ToString(bytes32(_msg.index(25, 32)));
tokenSymbol = bytes32ToString(bytes32(_msg.index(153, 32)));
price = uint128(_msg.indexUint(185, 16));
}

// TODO: should be moved to a util contract
Expand Down Expand Up @@ -95,48 +106,82 @@ library ConnectorMessages {
*
* 0: call type (uint8 = 1 byte)
* 1-8: poolId (uint64 = 8 bytes)
* 9-25: trancheId (16 bytes)
* 26-46: user (Ethereum address, 20 bytes)
* 47-78: validUntil (uint256 = 32 bytes)
* 9-24: trancheId (16 bytes)
* 25-45: user (Ethereum address, 20 bytes - Skip 12 bytes from 32-byte addresses)
* 57-65: validUntil (uint64 = 8 bytes)
*
* TODO: use bytes32 for user (for non-EVM compatibility)
*/
function formatUpdateMember(uint64 poolId, bytes16 trancheId, address user, uint256 validUntil) internal pure returns (bytes memory) {
return abi.encodePacked(uint8(Call.UpdateMember), poolId, trancheId, user, validUntil);
function formatUpdateMember(uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) internal pure returns (bytes memory) {
// NOTE: Since parseUpdateMember parses the first 20 bytes of `user` and skips the following 12
// here we need to append 12 zeros to make it right. Drop once we support 32-byte addresses.
return abi.encodePacked(uint8(Call.UpdateMember), poolId, trancheId, user, bytes(hex"000000000000000000000000"), validUntil);
}

function isUpdateMember(bytes29 _msg) internal pure returns (bool) {
return messageType(_msg) == Call.UpdateMember;
}

function parseUpdateMember(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, address user, uint256 validUntil) {
function parseUpdateMember(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) {
poolId = uint64(_msg.indexUint(1, 8));
trancheId = bytes16(_msg.index(9, 16));
user = address(bytes20(_msg.index(25, 20)));
// TODO: skip 12 padded zeroes from address
validUntil = uint256(_msg.index(45, 32));
validUntil = uint64(_msg.indexUint(57, 8));
}

/**
* Update token price
*
* 0: call type (uint8 = 1 byte)
* 1-8: poolId (uint64 = 8 bytes)
* 9-25: trancheId (16 bytes)
* 26-58: price (uint256 = 32 bytes)
* 9-24: trancheId (16 bytes)
* 25-41: price (uint128 = 16 bytes)
*/
function formatUpdateTokenPrice(uint64 poolId, bytes16 trancheId, uint256 price) internal pure returns (bytes memory) {
function formatUpdateTokenPrice(uint64 poolId, bytes16 trancheId, uint128 price) internal pure returns (bytes memory) {
return abi.encodePacked(uint8(Call.UpdateTokenPrice), poolId, trancheId, price);
}

function isUpdateTokenPrice(bytes29 _msg) internal pure returns (bool) {
return messageType(_msg) == Call.UpdateTokenPrice;
}

function parseUpdateTokenPrice(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, uint256 price) {
function parseUpdateTokenPrice(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, uint128 price) {
poolId = uint64(_msg.indexUint(1, 8));
trancheId = bytes16(_msg.index(9, 16));
price = uint256(_msg.index(25, 32));
price = uint128(_msg.indexUint(25, 16));
}

/**
* Transfer
*
* 0: call type (uint8 = 1 byte)
* 1-8: poolId (uint64 = 8 bytes)
* 9-25: trancheId (16 bytes)
* 26-46: user (Ethereum address, 20 bytes)
* 47-78: amount (uint256 = 32 bytes)
*
*/
function formatTransfer(uint64 poolId, bytes16 trancheId, address user, uint256 amount, bytes9 destinationDomain) internal pure returns (bytes memory) {
return abi.encodePacked(uint8(Call.Transfer), poolId, trancheId, user, amount, destinationDomain);
}

function isTransfer(bytes29 _msg) internal pure returns (bool) {
return messageType(_msg) == Call.Transfer;
}

function parseTransfer(bytes29 _msg) internal pure returns (uint64 poolId, bytes16 trancheId, address user, uint256 amount) {
hieronx marked this conversation as resolved.
Show resolved Hide resolved
poolId = uint64(_msg.indexUint(1, 8));
trancheId = bytes16(_msg.index(9, 16));
user = address(bytes20(_msg.index(25, 20)));
amount = uint256(_msg.index(45, 32));
}

function formatDomain(Domain domain) public pure returns (bytes9) {
return bytes9(byte(uint8(domain)));
}

function formatDomain(Domain domain, uint64 domainId) public pure returns (bytes9) {
return bytes9(abi.encodePacked(uint8(domain), domainId).ref(0).index(0, 9));
}

}
54 changes: 41 additions & 13 deletions src/routers/xcm/Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import {ConnectorMessages} from "../../Messages.sol";

interface ConnectorLike {
function addPool(uint64 poolId) external;
function addTranche(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol) external;
function updateMember(uint64 poolId, bytes16 trancheId, address user, uint256 validUntil) external;
function updateTokenPrice(uint64 poolId, bytes16 trancheId, uint256 price) external;
function addTranche(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol, uint128 price) external;
function updateMember(uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) external;
function updateTokenPrice(uint64 poolId, bytes16 trancheId, uint128 price) external;
function handleTransfer(uint64 poolId, bytes16 trancheId, address user, uint256 amount) external;
}

contract ConnectorXCMRouter {
Expand All @@ -26,30 +27,57 @@ contract ConnectorXCMRouter {
connector = ConnectorLike(connector_);
centrifugeChainOrigin = centrifugeChainOrigin_;
}



modifier onlyCentrifugeChainOrigin() {
require(msg.sender == address(centrifugeChainOrigin), "ConnectorXCMRouter/invalid-origin");
_;
}

modifier onlyConnector() {
require(msg.sender == address(connector), "ConnectorXCMRouter/only-connector-allowed-to-call");
_;
}

function handle(
bytes memory _message
) external onlyCentrifugeChainOrigin {
bytes29 _msg = _message.ref(0);
if (ConnectorMessages.isAddPool(_msg) == true) {
if (ConnectorMessages.isAddPool(_msg)) {
uint64 poolId = ConnectorMessages.parseAddPool(_msg);
connector.addPool(poolId);
} else if (ConnectorMessages.isAddTranche(_msg) == true) {
(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol) = ConnectorMessages.parseAddTranche(_msg);
connector.addTranche(poolId, trancheId, tokenName, tokenSymbol);
} else if (ConnectorMessages.isUpdateMember(_msg) == true) {
(uint64 poolId, bytes16 trancheId, address user, uint256 amount) = ConnectorMessages.parseUpdateMember(_msg);
connector.updateMember(poolId, trancheId, user, amount);
} else if (ConnectorMessages.isUpdateTokenPrice(_msg) == true) {
(uint64 poolId, bytes16 trancheId, uint256 price) = ConnectorMessages.parseUpdateTokenPrice(_msg);
} else if (ConnectorMessages.isAddTranche(_msg)) {
(uint64 poolId, bytes16 trancheId, string memory tokenName, string memory tokenSymbol, uint128 price) = ConnectorMessages.parseAddTranche(_msg);
connector.addTranche(poolId, trancheId, tokenName, tokenSymbol, price);
} else if (ConnectorMessages.isUpdateMember(_msg)) {
(uint64 poolId, bytes16 trancheId, address user, uint64 validUntil) = ConnectorMessages.parseUpdateMember(_msg);
connector.updateMember(poolId, trancheId, user, validUntil);
} else if (ConnectorMessages.isUpdateTokenPrice(_msg)) {
(uint64 poolId, bytes16 trancheId, uint128 price) = ConnectorMessages.parseUpdateTokenPrice(_msg);
connector.updateTokenPrice(poolId, trancheId, price);
} else if (ConnectorMessages.isTransfer(_msg)) {
(uint64 poolId, bytes16 trancheId, address user, uint256 amount) = ConnectorMessages.parseTransfer(_msg);
connector.handleTransfer(poolId, trancheId, user, amount);
} else {
require(false, "invalid-message");
}
}

function sendMessage(uint64 poolId, bytes16 trancheId, uint256 amount, address user) external onlyConnector {
// TODO: implement
NunoAlexandre marked this conversation as resolved.
Show resolved Hide resolved
}

function bytes32ToString(bytes32 _bytes32) internal pure returns (string memory) {
uint8 i = 0;
while (i < 32 && _bytes32[i] != 0) {
i++;
}

bytes memory bytesArray = new bytes(i);
for (i = 0; i < 32 && _bytes32[i] != 0; i++) {
bytesArray[i] = _bytes32[i];
}
return string(bytesArray);
}

}
5 changes: 5 additions & 0 deletions src/token/restricted.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ interface ERC20Like {
function mint(address usr, uint wad) external;
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function balanceOf(address usr) external view returns (uint wad);
function burn(address usr, uint wad) external;
function transferFrom(address from, address to, uint amount) external returns (bool);
function totalSupply() external returns (uint);
function approve(address _spender, uint256 _value) external returns (bool);
}

interface RestrictedTokenLike is ERC20Like {
Expand Down
Loading