Skip to content

Commit 89ed667

Browse files
Predicate contract for PoS bridge integration
1 parent a1ce123 commit 89ed667

File tree

8 files changed

+285
-1
lines changed

8 files changed

+285
-1
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@
1515
path = lib/@uniswap/v3-core
1616
url = https://github.com/Uniswap/v3-core
1717
branch = 0.8
18+
[submodule "lib/pos-portal"]
19+
path = lib/pos-portal
20+
url = https://github.com/maticnetwork/pos-portal

lib/pos-portal

Submodule pos-portal added at 8460b1f

remappings.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
@openzeppelin/contracts/=lib/@openzeppelin/contracts/contracts/
22
@gnosis/auction/=lib/@gnosis/auction/contracts/
3+
@polygon/pos-portal/=lib/pos-portal/contracts/
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
pragma solidity 0.8.15;
4+
5+
import "../../BobToken.sol";
6+
7+
/**
8+
* @title PolygonBobToken
9+
*/
10+
contract PolygonBobToken is BobToken {
11+
event Withdrawn(address indexed account, uint256 value);
12+
13+
constructor(address _self) BobToken(_self) {}
14+
15+
function deposit(address _user, bytes calldata _depositData) external {
16+
mint(_user, abi.decode(_depositData, (uint256)));
17+
}
18+
19+
function withdraw(uint256 _amount) external {
20+
_burn(msg.sender, _amount);
21+
22+
emit Withdrawn(msg.sender, _amount);
23+
}
24+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.6.6;
4+
5+
import "@polygon/pos-portal/root/TokenPredicates/ITokenPredicate.sol";
6+
7+
interface IERC20MintBurn {
8+
function mint(address user, uint256 amount) external;
9+
function burn(uint256 amount) external;
10+
function burnFrom(address user, uint256 amount) external;
11+
}
12+
13+
/**
14+
* @title ERC20 Mint/Burn Predicate for Polygon PoS bridge.
15+
* Works with a `Withdrawn(address account, uint256 value)` event.
16+
*/
17+
contract PolygonERC20MintBurnPredicate is ITokenPredicate {
18+
using RLPReader for bytes;
19+
using RLPReader for RLPReader.RLPItem;
20+
21+
event LockedERC20(
22+
address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256 amount
23+
);
24+
25+
// keccak256("Withdrawn(address account, uint256 value)");
26+
bytes32 public constant WITHDRAWN_EVENT_SIG = 0x7084f5476618d8e60b11ef0d7d3f06914655adb8793e28ff7f018d4c76d505d5;
27+
28+
// see https://github.com/maticnetwork/pos-portal/blob/master/contracts/root/RootChainManager/RootChainManager.sol
29+
address public immutable rootChainManager;
30+
31+
constructor(address _rootChainManager) public {
32+
rootChainManager = _rootChainManager;
33+
}
34+
35+
/**
36+
* Burns ERC20 tokens for deposit.
37+
* @dev Reverts if not called by the manager (RootChainManager).
38+
* @param depositor Address who wants to deposit tokens.
39+
* @param depositReceiver Address (address) who wants to receive tokens on child chain.
40+
* @param rootToken Token which gets deposited.
41+
* @param depositData ABI encoded amount.
42+
*/
43+
function lockTokens(
44+
address depositor,
45+
address depositReceiver,
46+
address rootToken,
47+
bytes calldata depositData
48+
)
49+
external
50+
override
51+
{
52+
require(msg.sender == rootChainManager, "Predicate: only manager");
53+
uint256 amount = abi.decode(depositData, (uint256));
54+
emit LockedERC20(depositor, depositReceiver, rootToken, amount);
55+
IERC20MintBurn(rootToken).burnFrom(depositor, amount);
56+
}
57+
58+
/**
59+
* Validates the {Withdrawn} log signature, then mints the correct amount to withdrawer.
60+
* @dev Reverts if not called only by the manager (RootChainManager).
61+
* @param rootToken Token which gets withdrawn
62+
* @param log Valid ERC20 burn log from child chain
63+
*/
64+
function exitTokens(address, address rootToken, bytes memory log) public override {
65+
require(msg.sender == rootChainManager, "Predicate: only manager");
66+
67+
RLPReader.RLPItem[] memory logRLPList = log.toRlpItem().toList();
68+
RLPReader.RLPItem[] memory logTopicRLPList = logRLPList[1].toList(); // topics
69+
70+
require(
71+
bytes32(logTopicRLPList[0].toUint()) == WITHDRAWN_EVENT_SIG, // topic0 is event sig
72+
"Predicate: invalid signature"
73+
);
74+
75+
address withdrawer = address(logTopicRLPList[1].toUint()); // topic1 is from address
76+
77+
IERC20MintBurn(rootToken).mint(withdrawer, logRLPList[2].toUint());
78+
}
79+
}

src/interfaces/IBurnableERC20.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pragma solidity 0.8.15;
44

55
interface IBurnableERC20 {
66
function burn(uint256 amount) external;
7+
function burnFrom(address user, uint256 amount) external;
78
}

src/token/ERC20MintBurn.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ abstract contract ERC20MintBurn is IMintableERC20, IBurnableERC20, Ownable, Base
4242
* @param _to address of the tokens receiver.
4343
* @param _amount amount of tokens to mint.
4444
*/
45-
function mint(address _to, uint256 _amount) external {
45+
function mint(address _to, uint256 _amount) public {
4646
require(isMinter(msg.sender), "ERC20MintBurn: not a minter");
4747

4848
_mint(_to, _amount);
@@ -58,4 +58,18 @@ abstract contract ERC20MintBurn is IMintableERC20, IBurnableERC20, Ownable, Base
5858

5959
_burn(msg.sender, _value);
6060
}
61+
62+
/**
63+
* @dev Burns pre-approved tokens from the other address.
64+
* Callable only by one of the burner addresses.
65+
* @param _from account to burn tokens from.
66+
* @param _value amount of tokens to burn. Should be less than or equal to caller balance.
67+
*/
68+
function burnFrom(address _from, uint256 _value) external virtual {
69+
require(isBurner(msg.sender), "ERC20MintBurn: not a burner");
70+
71+
_spendAllowance(_from, msg.sender, _value);
72+
73+
_burn(_from, _value);
74+
}
6175
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
pragma solidity 0.8.15;
4+
5+
import "forge-std/Test.sol";
6+
import "../../shared/Env.t.sol";
7+
import "../../../src/bridge/polygon/PolygonBobToken.sol";
8+
import "../../../src/proxy/EIP1967Proxy.sol";
9+
10+
interface IRootChainManager {
11+
function registerPredicate(bytes32 tokenType, address predicate) external;
12+
function mapToken(address rootToken, address childToken, bytes32 tokenType) external;
13+
function depositFor(address user, address rootToken, bytes memory depositData) external;
14+
}
15+
16+
interface IChildChainManager {
17+
function onStateReceive(uint256, bytes calldata data) external;
18+
}
19+
20+
interface IBobPredicate {
21+
function exitTokens(address, address rootToken, bytes memory log) external;
22+
}
23+
24+
contract PolygonPoSBridge is Test {
25+
event LockedERC20(
26+
address indexed depositor, address indexed depositReceiver, address indexed rootToken, uint256 amount
27+
);
28+
event Withdrawn(address indexed account, uint256 value);
29+
30+
BobToken bobMainnet;
31+
PolygonBobToken bobPolygon;
32+
33+
uint256 mainnetFork;
34+
uint256 polygonFork;
35+
36+
address bobPredicate;
37+
address rootChainManager = 0xA0c68C638235ee32657e8f720a23ceC1bFc77C77;
38+
address rootChainManagerOwner = 0xFa7D2a996aC6350f4b56C043112Da0366a59b74c;
39+
address childChainManager = 0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa;
40+
address stateSyncer = 0x0000000000000000000000000000000000001001;
41+
42+
function setUp() public {
43+
mainnetFork = vm.createFork(forkRpcUrlMainnet);
44+
polygonFork = vm.createFork(forkRpcUrlPolygon);
45+
46+
vm.selectFork(mainnetFork);
47+
48+
EIP1967Proxy bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
49+
BobToken bobImpl = new BobToken(address(bobProxy));
50+
bobProxy.upgradeTo(address(bobImpl));
51+
bobMainnet = BobToken(address(bobProxy));
52+
53+
bobMainnet.updateMinter(address(this), true, false);
54+
55+
vm.selectFork(polygonFork);
56+
57+
bobProxy = new EIP1967Proxy(address(this), mockImpl, "");
58+
PolygonBobToken bobImpl2 = new PolygonBobToken(address(bobProxy));
59+
bobProxy.upgradeTo(address(bobImpl2));
60+
bobPolygon = PolygonBobToken(address(bobProxy));
61+
62+
bobPolygon.updateMinter(address(this), true, false);
63+
bobPolygon.updateMinter(childChainManager, true, true);
64+
65+
vm.selectFork(mainnetFork);
66+
67+
vm.etch(rootChainManagerOwner, "");
68+
vm.startPrank(rootChainManagerOwner);
69+
bytes memory predicateCode = bytes.concat(
70+
vm.getCode("out/PolygonERC20MintBurnPredicate.sol/PolygonERC20MintBurnPredicate.json"),
71+
abi.encode(rootChainManager)
72+
);
73+
assembly {
74+
sstore(bobPredicate.slot, create(0, add(predicateCode, 32), mload(predicateCode)))
75+
}
76+
IRootChainManager(rootChainManager).registerPredicate(keccak256("BOB"), bobPredicate);
77+
vm.recordLogs();
78+
IRootChainManager(rootChainManager).mapToken(address(bobMainnet), address(bobPolygon), keccak256("BOB"));
79+
vm.stopPrank();
80+
_syncState();
81+
82+
bobMainnet.updateMinter(bobPredicate, true, true);
83+
84+
vm.label(address(bobMainnet), "BOB");
85+
vm.label(address(bobPolygon), "BOB");
86+
}
87+
88+
function _syncState() internal {
89+
uint256 curFork = vm.activeFork();
90+
Vm.Log[] memory logs = vm.getRecordedLogs();
91+
vm.selectFork(polygonFork);
92+
for (uint256 i = 0; i < logs.length; i++) {
93+
if (logs[i].topics[0] == bytes32(0x103fed9db65eac19c4d870f49ab7520fe03b99f1838e5996caf47e9e43308392)) {
94+
vm.prank(stateSyncer);
95+
IChildChainManager(childChainManager).onStateReceive(0, abi.decode(logs[i].data, (bytes)));
96+
}
97+
}
98+
vm.selectFork(curFork);
99+
}
100+
101+
function testBridgeToPolygon() public {
102+
vm.selectFork(mainnetFork);
103+
104+
bobMainnet.mint(user1, 100 ether);
105+
106+
vm.startPrank(user1);
107+
bobMainnet.approve(bobPredicate, 10 ether);
108+
vm.expectEmit(true, true, true, true, bobPredicate);
109+
emit LockedERC20(user1, user2, address(bobMainnet), 10 ether);
110+
vm.recordLogs();
111+
IRootChainManager(rootChainManager).depositFor(user2, address(bobMainnet), abi.encode(10 ether));
112+
vm.stopPrank();
113+
114+
_syncState();
115+
116+
assertEq(bobMainnet.totalSupply(), 90 ether);
117+
assertEq(bobMainnet.balanceOf(user1), 90 ether);
118+
119+
vm.selectFork(polygonFork);
120+
121+
assertEq(bobPolygon.totalSupply(), 10 ether);
122+
assertEq(bobPolygon.balanceOf(user2), 10 ether);
123+
}
124+
125+
function testBridgeFromPolygon() public {
126+
vm.selectFork(polygonFork);
127+
128+
bobPolygon.mint(user1, 100 ether);
129+
130+
vm.prank(user1);
131+
vm.expectEmit(true, false, false, true, address(bobPolygon));
132+
emit Withdrawn(user1, 10 ether);
133+
bobPolygon.withdraw(10 ether);
134+
135+
vm.selectFork(mainnetFork);
136+
137+
// cast --to-rlp '["<token>", ["<topic0>", "<topic1>"], "<data>"]'
138+
bytes memory logRLP = bytes.concat(
139+
hex"f87a94",
140+
abi.encodePacked(address(bobPolygon)),
141+
hex"f842a0",
142+
keccak256("Withdrawn(address,uint256)"),
143+
hex"a0",
144+
abi.encode(user1),
145+
hex"a0",
146+
abi.encode(10 ether)
147+
);
148+
149+
vm.etch(rootChainManager, "");
150+
vm.prank(rootChainManager);
151+
IBobPredicate(bobPredicate).exitTokens(user1, address(bobMainnet), logRLP);
152+
153+
assertEq(bobMainnet.totalSupply(), 10 ether);
154+
assertEq(bobMainnet.balanceOf(user1), 10 ether);
155+
156+
vm.selectFork(polygonFork);
157+
158+
assertEq(bobPolygon.totalSupply(), 90 ether);
159+
assertEq(bobPolygon.balanceOf(user1), 90 ether);
160+
}
161+
}

0 commit comments

Comments
 (0)