Skip to content

Commit 76b1d45

Browse files
authored
Merge pull request #21 from openfort-xyz/feat/fallback_prover
wip
2 parents 8f6c645 + f915833 commit 76b1d45

File tree

9 files changed

+105
-73
lines changed

9 files changed

+105
-73
lines changed

README.md

Lines changed: 39 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
# Openfort Ecosystem Abstraction
22

33
## Overview
4-
Ecosystems are parent entities for groups of apps operating across different blockchains or standalone layer 2 networks. Openfort [**ecosystem wallets**](https://www.openfort.xyz/docs/guides/ecosystem) enable seamless interoperability between applications, allowing ecosystems to design their ideal, unified wallet experience. The next evolution is consolidating user liquidity across apps, providing a single, unified balance instantly spendable across the ecosystem. This vision will be powered by Openfort's chain abstraction implementation based on [MagicSpend++](https://ethresear.ch/t/magicspend-spend-now-debit-later/19678/9) hosted in this repository.
4+
Ecosystems are parent entities for groups of apps operating across different blockchains or standalone layer 2 networks. Openfort [**ecosystem wallets**](https://www.openfort.xyz/docs/guides/ecosystem) enable seamless interoperability between applications, allowing ecosystems to design their ideal, unified wallet experience. The next evolution is consolidating user liquidity across apps, providing a single, unified balance instantly spendable across the ecosystem. This vision will be powered by Openfort's chain abstraction implementation of [MagicSpend++](https://ethresear.ch/t/magicspend-spend-now-debit-later/19678/9) hosted in this repository.
55

66
With this setup, ecosystems can deploy tailor-made 4337 chain abstraction infrastructure.
77
They become Liquidity Providers (LPs) for their users, sharing with them the value that would otherwise have been captured by solvers/fillers.
88
They own their users' experience from the wallet to the chain.
99

1010

11-
You will find *at least* the following contracts:
12-
13-
* Chain Abstraction Paymaster
14-
* Time-locked Vault
15-
* Vault Manager
16-
* Invoice Manager
17-
1811
## System Architecture
1912

2013
![architecture](./assets/archi.jpg)
@@ -23,69 +16,59 @@ You will find *at least* the following contracts:
2316

2417
![paymasterAndData](./assets/paymasterAndData.png)
2518

26-
## System Components & assumptions
19+
## System Components
2720

2821
### Time-locked Vault
29-
- Define locking period
30-
- Deploy on any chain
31-
- Define any ERC20 / native asset (each asset must have a correspondence in dollars)
32-
- Define the yield strategy
33-
34-
_Note:_ "locking" can be simplified into a **SEND** transaction from an EOA to the Smart Contract Account. A backend watcher service could listen for `received` events and automatically lock the funds (i.e., transfer them to the time-locked vault). This approach would require users to sign a session key for the watcher service.
35-
36-
37-
### Chain Abstraction Paymaster
38-
39-
The Paymaster fronts the funds on the destination chain for the user if they _HAVE_ enough locked balance (checked by Openfort Backend).
40-
The Paymaster contract will then be reimbursed on the source chain(s). Ni1o: user has 100@A, 50@B, and spends 130@C
22+
- Tokenized Vaults with a single underlying EIP-20 token
23+
- *not* [4626](https://eips.ethereum.org/EIPS/eip-4626) compliant (does *not* implement EIP-20 to represent shares)
24+
- only the VaultManager can interact with the Vault
25+
- Define locking period when initialiaing the vault
26+
- Deploy on any supported source chains
27+
- Can be yield-bearing (e.g deposit to Aaver or Morpho)
4128

42-
* Set/update the Paymaster owner address (ecosystem *MUST* own the Paymaster).
43-
* Fund/withdraw Paymaster balance (Openfort crafts the transaction, but the ecosystem owner _MUST_ sign it).
44-
* Ragequit > withdraw all funds from all Paymasters
45-
* Receive webhook alerts when the Paymaster balance falls below a certain threshold to enable rebalancing.
29+
_Note:_ "locking" can be simplified into a **SEND** transaction from an EOA to the Smart Contract Account. A backend watcher listens for `received` events and automatically lock the funds (i.e., transfer them to the time-locked vault). This approach requires users to sign a session key for the watcher service.
4630

47-
### Trust assumptions
31+
### Vault Manager
32+
- Manage Vaults
33+
- Manage withdrawals and deposits
4834

49-
* The system relies on cross-L2 execution proofs enabled by [Polymer](https://docs.polymerlabs.org/docs/build/examples/chain_abstraction/), eliminating the need for Users to trust Openfort or the Ecosystem. To recover funds locked in source chain vaults on behalf of the ecosystem, Openfort must prove the execution of the userOp on the remote chain. There is no refund on source chain without the corresponding remote chain execution proof. The InvoiceManager track invoices onchain to prevent double-refund.
50-
* Openfort does not have custody of funds in the Ecosystem Paymaster because the userOp is co-signed within a secure enclave, following predefined policies set by the ecosystem.
35+
### Invoice Manager
36+
- Settlement of invoices
37+
- Prevent double-repayment of invoices with state proof verification
38+
- authorize paymasters and paymaster verifiers
5139

40+
### Chain Abstraction Paymaster (CABPaymaster)
5241

53-
### Onchain deployments
42+
The CABPaymaster fronts funds on the destination chain for the user if they _HAVE_ enough locked balance (checked by Openfort Backend).
5443

55-
One time action by Openfort: Deploy Vaults, VaultManager and InvoiceManager.
44+
The Paymaster contract will get repaid on the source chain(s). Ni1o: user has 100@A, 50@B, and spends 130@C
5645

57-
->> LP as a Service: Deploy the Chain Abstraction Paymaster *on each* blockchain supported by the ecosystem.
46+
- Set/update the Paymaster owner address (ecosystem *MUST* own the Paymaster)
47+
- Fund/withdraw Paymaster balance (Openfort crafts the transaction, but the ecosystem owner _MUST_ sign it)
48+
- Ragequit > owner withdraw all funds from all Paymasters with one signature
5849

50+
Paymaster Owner can subscribe to webhook alerts when the Paymaster balance falls below a certain threshold, before automatic rebalancing is implemented.
5951

60-
# Local Development
52+
### Paymaster Verifiers
53+
- Permissionless verification of remote event (InvoiceCreated) or storage proof (invoices mapping in the invoiceManager)
54+
- Permissionless verification of invoice
6155

56+
As part of chain abstraction activation, an account registers a Paymaster Verifier, which is subsequently called by the InvoiceManager before processing repayments.
6257

63-
Launch tests:
64-
```
65-
forge test
66-
```
58+
One of the system’s key strengths is its modular approach to proof verification. We envision a future where state proofs play a crucial role in Ethereum interoperability, with more proof providers emerging. Our design allows for seamless integration of new proof verification strategies, giving advanced users the flexibility to choose the one that best suits their use case.
6759

68-
Deploy local node:
69-
```
70-
anvil
71-
```
60+
## Trust assumptions
7261

73-
Deploy two mock ERC20:
62+
* The system relies on cross-L2 execution proofs enabled by [Polymer](https://docs.polymerlabs.org/docs/build/examples/chain_abstraction/), eliminating the need for Users to trust Openfort or the Ecosystem. To recover funds locked in source chain vaults on behalf of the ecosystem, Openfort must prove the execution of the userOp on the remote chain. There is no refund on source chain without the corresponding remote chain execution proof. The InvoiceManager tracks invoices onchain to prevent double-refund.
63+
* The system supports [Hashi](https://crosschain-alliance.gitbook.io/hashi/introduction/what-is-hashi) as a fallback proving mechanism if Polymer or Openfort cease operations. Liquidity providers (LPs) can generate a proof for their fronted funds by running a [Hashi RPC API locally](https://github.com/gnosis/hashi/tree/main/packages/rpc#getting-started) and submit it to `fallbackRepay` along with the invoice. This enables refunds using only public data, without relying on any third party. Note that the fallback proving strategy might evolve in the future but will always remain permissionless.
64+
* Openfort does not have custody of funds in the Ecosystem Paymaster because the userOp is co-signed within a secure enclave executing predefined policies set by the ecosystem. At any time, the ecosystem can disable a signer to immediately block any new userOp from being sent.
7465

75-
```
76-
forge create src/mocks/MockERC20.sol:MockERC20 --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
77-
forge create src/mocks/MockERC20.sol:MockERC20 --private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
78-
```
66+
## Deployments
7967

68+
- One InvoiceManager owned by Openfort
69+
- One VaultManager onwed by Openfort
70+
- One CABPaymaster per Ecosystem and Owned by the Ecosystem
71+
- All Vaults are owned by Openfort
72+
- All Paymaster Verifiers
8073

81-
Deploy Chain Abstraction Setup including Invoice Manager, two tokenized vaults, Vault Manager and Paymaster:
82-
```
83-
PK_DEPLOYER=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
84-
CROSS_L2_PROVER=0xBA3647D0749Cb37CD92Cc98e6185A77a8DCBFC62
85-
OWNER=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
86-
WITHDRAW_LOCK_BLOCK=100
87-
VERIFYING_SIGNER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
88-
VERSION_SALT=0x6660000000000000000000000000000000000000000000000000000000000000
89-
ENTRY_POINT_ADDRESS=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
90-
forge script script/deployChainAbstractionSetup.s.sol:DeployChainAbstractionSetup "[0xusdc, 0xusdt]" --sig "run(address[])" --rpc-url=127.0.0.1:854
91-
```
74+
Check latest deployment of the [demo cli](demo/constants.ts).

contracts/core/InvoiceManager.sol

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {IVaultManager} from "../interfaces/IVaultManager.sol";
4242
contract InvoiceManager is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardUpgradeable, IInvoiceManager {
4343
IVaultManager public vaultManager;
4444

45+
IPaymasterVerifier public fallbackPaymasterVerifier;
46+
4547
/// @notice Settlememt storage location used as the base for the invoices mapping following EIP-7201.
4648
// keccak256(abi.encode(uint256(keccak256(bytes("Dialy"))) - 1)) & ~bytes32(uint256(0xff))
4749
bytes32 private constant SETTLEMENT_STORAGE_LOCATION = 0x1574f0d0c24265911bc4961cda61aadd6e06faacad4bf42a2f89fb53fed1c800;
@@ -62,17 +64,22 @@ contract InvoiceManager is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardU
6264
_disableInitializers();
6365
}
6466

65-
function initialize(address initialOwner, IVaultManager _vaultManager) public virtual initializer {
67+
function initialize(address initialOwner, IVaultManager _vaultManager, IPaymasterVerifier _fallbackPaymasterVerifier)
68+
public
69+
virtual
70+
initializer
71+
{
6672
__Ownable_init(initialOwner);
6773
__ReentrancyGuard_init();
6874

6975
vaultManager = _vaultManager;
76+
fallbackPaymasterVerifier = _fallbackPaymasterVerifier;
7077
}
7178

7279
function _authorizeUpgrade(address) internal override onlyOwner {}
7380

7481
modifier onlyPaymaster(address account) {
75-
require(cabPaymasters[account].paymaster == msg.sender, "InvoiceManager: caller is not the paymaster");
82+
require(cabPaymasters[account].paymaster == msg.sender, "InvoiceManager: unauthorized paymaster");
7683
_;
7784
}
7885

@@ -119,14 +126,23 @@ contract InvoiceManager is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardU
119126
require(!isInvoiceRepaid[invoiceId], "InvoiceManager: invoice already repaid");
120127

121128
bool isVerified = paymasterVerifier.verifyInvoice(invoiceId, invoice, proof);
122-
123129
if (!isVerified) revert("InvoiceManager: invalid invoice");
124-
(IVault[] memory vaults, uint256[] memory amounts) = _getRepayToken(invoice);
125130

126-
isInvoiceRepaid[invoiceId] = true;
127-
vaultManager.withdrawSponsorToken(invoice.account, vaults, amounts, invoice.paymaster);
131+
_repay(invoiceId, invoice);
132+
}
128133

129-
emit InvoiceRepaid(invoiceId, invoice.account, invoice.paymaster);
134+
/// @inheritdoc IInvoiceManager
135+
function fallbackRepay(bytes32 invoiceId, InvoiceWithRepayTokens calldata invoice, bytes calldata proof)
136+
external
137+
override
138+
nonReentrant
139+
{
140+
require(!isInvoiceRepaid[invoiceId], "InvoiceManager: invoice already repaid");
141+
142+
bool isVerified = fallbackPaymasterVerifier.verifyInvoice(invoiceId, invoice, proof);
143+
if (!isVerified) revert("InvoiceManager: invalid invoice");
144+
145+
_repay(invoiceId, invoice);
130146
}
131147

132148
/// @inheritdoc IInvoiceManager
@@ -192,4 +208,13 @@ contract InvoiceManager is UUPSUpgradeable, OwnableUpgradeable, ReentrancyGuardU
192208
$.slot := SETTLEMENT_STORAGE_LOCATION
193209
}
194210
}
211+
212+
function _repay(bytes32 invoiceId, InvoiceWithRepayTokens calldata invoice) internal {
213+
(IVault[] memory vaults, uint256[] memory amounts) = _getRepayToken(invoice);
214+
215+
isInvoiceRepaid[invoiceId] = true;
216+
vaultManager.withdrawSponsorToken(invoice.account, vaults, amounts, invoice.paymaster);
217+
218+
emit InvoiceRepaid(invoiceId, invoice.account, invoice.paymaster);
219+
}
195220
}

contracts/interfaces/IInvoiceManager.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,21 @@ interface IInvoiceManager {
9696
function createInvoice(uint256 nonce, address paymaster, bytes32 invoiceId) external;
9797

9898
/**
99-
* @notice Repay the invoice.
99+
* @notice Verify proof and repay the invoice.
100100
* @param invoiceId The ID of the invoice.
101101
* @param invoice The invoice to repay.
102102
* @param proof The proof of the repayment.
103103
*/
104104
function repay(bytes32 invoiceId, InvoiceWithRepayTokens calldata invoice, bytes calldata proof) external;
105105

106+
/**
107+
* @notice Verify proof with the fallback verifier and repay the invoice.
108+
* @param invoiceId The ID of the invoice.
109+
* @param invoice The invoice to repay.
110+
* @param proof The proof of the repayment.
111+
*/
112+
function fallbackRepay(bytes32 invoiceId, InvoiceWithRepayTokens calldata invoice, bytes calldata proof) external;
113+
106114
/**
107115
* @notice Withdraw the locked tokens to the account.
108116
* @param account The address of the account.

contracts/libraries/LibTokens.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ library LibTokens {
6161
}
6262

6363
function frontToken(address token, address recipient, uint256 amount) internal {
64+
//require(store.supported[token], "TokenManager: token not supported");
6465
// NOTE: use forceApprove to support tokens that require the approval
6566
// to be set to zero before setting it to a non-zero value, such as USDT.
6667
token == NATIVE_TOKEN ? _transferNative(recipient, amount) : IERC20(token).forceApprove(recipient, amount);

contracts/mocks/MockInvoiceManager.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ contract MockInvoiceManager is IMockInvoiceManager {
4040
revert("Not implemented");
4141
}
4242

43+
function fallbackRepay(bytes32, InvoiceWithRepayTokens calldata, bytes calldata) external {
44+
revert("Not implemented");
45+
}
46+
4347
function withdrawToAccount(address, IVault[] calldata, uint256[] calldata) external {
4448
revert("Not implemented");
4549
}

contracts/paymasters/CABPaymaster.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,11 @@ contract CABPaymaster is BasePaymaster, Initializable {
149149
parseSponsorTokenData(sponsorTokenData);
150150
for (uint8 i = 0; i < sponsorTokenLength;) {
151151
address token = sponsorTokens[i].token;
152+
152153
if (token != LibTokens.NATIVE_TOKEN) {
153154
require(IERC20(token).approve(sponsorTokens[i].spender, 0), "CABPaymaster: Reset approval failed");
154155
}
156+
155157
unchecked {
156158
++i;
157159
}

script/deployChainAbstractionSetup.s.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {IVaultManager} from "../contracts/interfaces/IVaultManager.sol";
66
import {Script, console} from "forge-std/Script.sol";
77

88
import {InvoiceManager} from "../contracts/core/InvoiceManager.sol";
9+
import {IPaymasterVerifier} from "../contracts/interfaces/IPaymasterVerifier.sol";
10+
911
import {CABPaymaster} from "../contracts/paymasters/CABPaymaster.sol";
1012
import {CABPaymasterFactory} from "../contracts/paymasters/CABPaymasterFactory.sol";
1113
import {UpgradeableOpenfortProxy} from "../contracts/proxy/UpgradeableOpenfortProxy.sol";
@@ -58,7 +60,9 @@ contract DeployChainAbstractionSetup is
5860
);
5961

6062
console.log("VaultManager Address", address(vaultManager));
61-
invoiceManager.initialize(owner, IVaultManager(address(vaultManager)));
63+
64+
IPaymasterVerifier hashiPaymasterVerifier = deployHashiPaymasterVerifier(address(invoiceManager), owner, versionSalt);
65+
invoiceManager.initialize(owner, IVaultManager(address(vaultManager)), hashiPaymasterVerifier);
6266

6367
for (uint256 i = 0; i < tokens.length; i++) {
6468
address token = tokens[i];
@@ -85,7 +89,6 @@ contract DeployChainAbstractionSetup is
8589
console.log("Paymaster Address", address(paymaster));
8690

8791
deployPolymerPaymasterVerifier(address(invoiceManager), owner, versionSalt);
88-
deployHashiPaymasterVerifier(address(invoiceManager), owner, versionSalt);
8992

9093
vm.stopBroadcast();
9194
}

script/deployHashiPaymasterVerifier.s.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import {Script, console} from "forge-std/Script.sol";
88
contract DeployHashiPaymasterVerifier is Script {
99
address internal shoyuBashi = vm.envAddress("SHOYU_BASHI");
1010

11-
function deployHashiPaymasterVerifier(address _invoiceManager, address _owner, bytes32 _versionSalt) public {
11+
function deployHashiPaymasterVerifier(address _invoiceManager, address _owner, bytes32 _versionSalt)
12+
public
13+
returns (HashiPaymasterVerifier)
14+
{
1215
HashiPaymasterVerifier hashiPaymasterVerifier =
1316
new HashiPaymasterVerifier{salt: _versionSalt}(IInvoiceManager(_invoiceManager), shoyuBashi, _owner);
1417
console.log("HashiPaymasterVerifier deployed at ", address(hashiPaymasterVerifier));
18+
return hashiPaymasterVerifier;
1519
}
1620
}

test/CABPaymater.t.sol

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {IPaymaster} from "account-abstraction/interfaces/IPaymaster.sol";
3333
import {MockInvoiceManager} from "../contracts/mocks/MockInvoiceManager.sol";
3434

3535
import {stdJson} from "forge-std/StdJson.sol";
36-
import {Test} from "forge-std/Test.sol";
36+
import {Test, console} from "forge-std/Test.sol";
3737

3838
contract CABPaymasterTest is Test {
3939
using stdJson for string;
@@ -104,12 +104,11 @@ contract CABPaymasterTest is Test {
104104
)
105105
);
106106

107-
invoiceManager.initialize(owner, IVaultManager(address(vaultManager)));
108-
109107
// NOTE: testing with mocked crossL2ProverV1 (real is deprecated)
110108
// gas report will not reflect the real cost of proving with polymerV1
111109
crossL2ProverV1 = ICrossL2Prover(address(new MockCrossL2Prover(address(invoiceManager))));
112110
// NOTE: testing with real crossL2ProverV2
111+
// https://docs.polymerlabs.org/docs/build/start
113112
crossL2ProverV2 = ICrossL2ProverV2(0xcDa03d74DEc5B24071D1799899B2e0653C24e5Fa);
114113

115114
// Initialize the supportedTokens array
@@ -122,14 +121,19 @@ contract CABPaymasterTest is Test {
122121

123122
mockERC20.mint(address(paymaster), PAYMASTER_BASE_MOCK_ERC20_BALANCE);
124123

125-
assertEq(address(invoiceManager.vaultManager()), address(vaultManager));
126124
assertEq(address(vaultManager.invoiceManager()), address(invoiceManager));
127125

128126
vm.startPrank(rekt);
129127

130128
polymerPaymasterVerifierV1 = new PolymerPaymasterVerifierV1(invoiceManager, crossL2ProverV1, owner);
131129
polymerPaymasterVerifierV2 = new PolymerPaymasterVerifierV2(IInvoiceManager(REAL_INVOICE_MANAGER), crossL2ProverV2, owner);
132130

131+
invoiceManager.initialize(
132+
owner, IVaultManager(address(vaultManager)), IPaymasterVerifier(address(polymerPaymasterVerifierV2))
133+
);
134+
135+
assertEq(address(invoiceManager.vaultManager()), address(vaultManager));
136+
133137
invoiceManager.registerPaymaster(
134138
address(paymaster), IPaymasterVerifier(address(polymerPaymasterVerifierV1)), block.timestamp + 100000
135139
);
@@ -341,8 +345,6 @@ contract CABPaymasterTest is Test {
341345
bytes32 expectedInvoiceId =
342346
invoiceManager.getInvoiceId(rekt, address(paymaster), userOp.nonce, BASE_SEPOLIA_CHAIN_ID, repayTokensBytes);
343347

344-
// don't know why comparison of paymaster address fails
345-
// even though it's the same address
346348
vm.expectEmit(true, true, true, false);
347349
emit IInvoiceManager.InvoiceCreated(expectedInvoiceId, rekt, address(paymaster));
348350
paymaster.postOp(IPaymaster.PostOpMode.opSucceeded, context, 1222, 42);

0 commit comments

Comments
 (0)