This repository has been archived by the owner on Oct 27, 2023. It is now read-only.
forked from 5afe/safe-core-protocol-demo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add event storage for metadata; Extend sample to show relaying * Add event for max fee update * Add posibility to relay tx via UI * Fix uri encoding * Improve UX slightly * Fix app url * Revert link change * Improve structure and documentation * Renaming * Formatting * Update description and logo for react app
- Loading branch information
Showing
33 changed files
with
1,443 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Safe{Core} Protocol Demo | ||
|
||
This repository contain a demo for developing and using [Plugins](https://github.com/5afe/safe-core-protocol-specs/tree/main/integrations#plugins) from the Safe{Core} Protocol. | ||
|
||
## Structure | ||
|
||
The repository is separated into two parts: | ||
|
||
- [Contracts](./contracts/) contains the sample contracts and scripts to deploy them | ||
- [Web App](./web/) contains the web app to configure and use the sample contracts | ||
|
||
## Make it your own | ||
|
||
To get started with your own plugin you can fork/ copy this repository and adjust the existing code. | ||
|
||
Follow the instructions in the [Contracts](./contracts/) folder to create a Plugin for your use case. You can then register the plugin on a test registry and it will be visible on [Demo App](https://5afe.github.io/safe-core-protocol-demo). | ||
|
||
This [Demo App](https://5afe.github.io/safe-core-protocol-demo) can be used as a [Safe app in the Safe{Wallet} Web](https://app.safe.global/share/safe-app?appUrl=https%3A%2F%2F5afe.github.io%2Fsafe-core-protocol-demo&chain=gor) interface to add the Plugin to a Safe. | ||
|
||
To configure your Plugin it is necessary to create a web app. For this follow the instructions in the [Web App](./web/) folder. A simple way to host your web app is to use [GitHub pages](https://pages.github.com/). For this you can use the `yarn deploy` script. | ||
|
||
Important don't forget to update your [Plugin Metadata](./contracts/README.md#plugin-metadata). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// SPDX-License-Identifier: LGPL-3.0-only | ||
pragma solidity ^0.8.18; | ||
|
||
import {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.sol"; | ||
|
||
enum MetadataProviderType { | ||
IPFS, | ||
URL, | ||
Contract, | ||
Event | ||
} | ||
|
||
interface IMetadataProvider { | ||
function retrieveMetadata(bytes32 metadataHash) external view returns (bytes memory metadata); | ||
} | ||
|
||
struct PluginMetadata { | ||
string name; | ||
string version; | ||
bool requiresRootAccess; | ||
string iconUrl; | ||
string appUrl; | ||
} | ||
|
||
library PluginMetadataOps { | ||
function encode(PluginMetadata memory data) internal pure returns (bytes memory) { | ||
return | ||
abi.encodePacked( | ||
uint8(0x00), // Format | ||
uint8(0x00), // Format version | ||
abi.encode(data.name, data.version, data.requiresRootAccess, data.iconUrl, data.appUrl) // Plugin Metadata | ||
); | ||
} | ||
|
||
function decode(bytes calldata data) internal pure returns (PluginMetadata memory) { | ||
require(bytes16(data[0:2]) == bytes16(0x0000), "Unsupported format or format version"); | ||
(string memory name, string memory version, bool requiresRootAccess, string memory iconUrl, string memory appUrl) = abi.decode( | ||
data[2:], | ||
(string, string, bool, string, string) | ||
); | ||
return PluginMetadata(name, version, requiresRootAccess, iconUrl, appUrl); | ||
} | ||
} | ||
|
||
abstract contract BasePlugin is ISafeProtocolPlugin { | ||
using PluginMetadataOps for PluginMetadata; | ||
|
||
string public name; | ||
string public version; | ||
bool public immutable requiresRootAccess; | ||
bytes32 public immutable metadataHash; | ||
|
||
constructor(PluginMetadata memory metadata) { | ||
name = metadata.name; | ||
version = metadata.version; | ||
requiresRootAccess = metadata.requiresRootAccess; | ||
metadataHash = keccak256(metadata.encode()); | ||
} | ||
} | ||
|
||
abstract contract BasePluginWithStoredMetadata is BasePlugin, IMetadataProvider { | ||
using PluginMetadataOps for PluginMetadata; | ||
|
||
bytes private encodedMetadata; | ||
|
||
constructor(PluginMetadata memory metadata) BasePlugin(metadata) { | ||
encodedMetadata = metadata.encode(); | ||
} | ||
|
||
function retrieveMetadata(bytes32 _metadataHash) external view override returns (bytes memory metadata) { | ||
require(metadataHash == _metadataHash, "Cannot retrieve metadata"); | ||
return encodedMetadata; | ||
} | ||
|
||
function metadataProvider() public view override returns (uint256 providerType, bytes memory location) { | ||
providerType = uint256(MetadataProviderType.Contract); | ||
location = abi.encode(address(this)); | ||
} | ||
} | ||
|
||
abstract contract BasePluginWithEventMetadata is BasePlugin { | ||
using PluginMetadataOps for PluginMetadata; | ||
|
||
event Metadata(bytes32 indexed metadataHash, bytes data); | ||
|
||
constructor(PluginMetadata memory metadata) BasePlugin(metadata) { | ||
emit Metadata(metadataHash, metadata.encode()); | ||
} | ||
|
||
function metadataProvider() public view override returns (uint256 providerType, bytes memory location) { | ||
providerType = uint256(MetadataProviderType.Event); | ||
location = abi.encode(address(this)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,94 +1,92 @@ | ||
// SPDX-License-Identifier: LGPL-3.0-only | ||
pragma solidity ^0.8.18; | ||
|
||
import {BasePluginWithEventMetadata, PluginMetadata} from "./Base.sol"; | ||
import {ISafe} from "@safe-global/safe-core-protocol/contracts/interfaces/Accounts.sol"; | ||
import {ISafeProtocolPlugin} from "@safe-global/safe-core-protocol/contracts/interfaces/Integrations.sol"; | ||
import {ISafeProtocolManager} from "@safe-global/safe-core-protocol/contracts/interfaces/Manager.sol"; | ||
import {SafeTransaction, SafeRootAccess} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; | ||
import {SafeTransaction, SafeProtocolAction} from "@safe-global/safe-core-protocol/contracts/DataTypes.sol"; | ||
import {_getFeeCollectorRelayContext, _getFeeTokenRelayContext, _getFeeRelayContext} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol"; | ||
|
||
enum MetadataProviderType { | ||
IPFS, | ||
URL, | ||
Contract, | ||
Event | ||
} | ||
|
||
interface MetadataProvider { | ||
function retrieveMetadata(bytes32 metadataHash) external view returns (bytes memory metadata); | ||
} | ||
|
||
struct PluginMetadata { | ||
string name; | ||
string version; | ||
bool requiresRootAccess; | ||
string iconUrl; | ||
string appUrl; | ||
} | ||
|
||
library PluginMetadataOps { | ||
function encode(PluginMetadata memory data) internal pure returns (bytes memory) { | ||
return | ||
abi.encodePacked( | ||
uint8(0x00), // Format | ||
uint8(0x00), // Format version | ||
abi.encode(data.name, data.version, data.requiresRootAccess, data.iconUrl, data.appUrl) // Plugin Metadata | ||
); | ||
} | ||
address constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; | ||
|
||
function decode(bytes calldata data) internal pure returns (PluginMetadata memory) { | ||
require(bytes16(data[0:2]) == bytes16(0x0000), "Unsupported format or format version"); | ||
(string memory name, string memory version, bool requiresRootAccess, string memory iconUrl, string memory appUrl) = abi.decode( | ||
data[2:], | ||
(string, string, bool, string, string) | ||
); | ||
return PluginMetadata(name, version, requiresRootAccess, iconUrl, appUrl); | ||
} | ||
} | ||
contract RelayPlugin is BasePluginWithEventMetadata { | ||
event MaxFeeUpdated(address indexed account, address indexed feeToken, uint256 maxFee); | ||
|
||
abstract contract BasePlugin is ISafeProtocolPlugin, MetadataProvider { | ||
using PluginMetadataOps for PluginMetadata; | ||
error FeeTooHigh(address feeToken, uint256 fee); | ||
error FeePaymentFailure(bytes data); | ||
error UntrustedOrigin(address origin); | ||
error RelayExecutionFailure(bytes data); | ||
error InvalidRelayMethod(bytes4 data); | ||
|
||
string public name; | ||
string public version; | ||
bool public immutable requiresRootAccess; | ||
bytes32 public immutable metadataHash; | ||
bytes private encodedMetadata; | ||
address public immutable trustedOrigin; | ||
bytes4 public immutable relayMethod; | ||
|
||
constructor(PluginMetadata memory metadata) { | ||
name = metadata.name; | ||
version = metadata.version; | ||
requiresRootAccess = metadata.requiresRootAccess; | ||
// Metadata Format + Format Version + Encoded Metadata | ||
encodedMetadata = metadata.encode(); | ||
metadataHash = keccak256(encodedMetadata); | ||
} | ||
// Account => token => maxFee | ||
mapping(address => mapping(address => uint256)) public maxFeePerToken; | ||
|
||
// TODO: Legacy version that should be removed | ||
function metaProvider() external view override returns (uint256 providerType, bytes memory location) { | ||
return metadataProvider(); | ||
constructor( | ||
address _trustedOrigin, | ||
bytes4 _relayMethod | ||
) | ||
BasePluginWithEventMetadata( | ||
PluginMetadata({ | ||
name: "Relay Plugin", | ||
version: "1.0.0", | ||
requiresRootAccess: false, | ||
iconUrl: "", | ||
appUrl: "https://5afe.github.io/safe-core-protocol-demo/#/relay/${plugin}" | ||
}) | ||
) | ||
{ | ||
trustedOrigin = _trustedOrigin; | ||
relayMethod = _relayMethod; | ||
} | ||
|
||
function metadataProvider() public view returns (uint256 providerType, bytes memory location) { | ||
providerType = uint256(MetadataProviderType.Contract); | ||
location = abi.encode(address(this)); | ||
function setMaxFeePerToken(address token, uint256 maxFee) external { | ||
maxFeePerToken[msg.sender][token] = maxFee; | ||
emit MaxFeeUpdated(msg.sender, token, maxFee); | ||
} | ||
|
||
function retrieveMetadata(bytes32 _metadataHash) external view returns (bytes memory metadata) { | ||
require(metadataHash == _metadataHash, "Cannot retrieve metadata"); | ||
return encodedMetadata; | ||
function payFee(ISafeProtocolManager manager, ISafe safe, uint256 nonce) internal { | ||
address feeCollector = _getFeeCollectorRelayContext(); | ||
address feeToken = _getFeeTokenRelayContext(); | ||
uint256 fee = _getFeeRelayContext(); | ||
SafeProtocolAction[] memory actions = new SafeProtocolAction[](1); | ||
uint256 maxFee = maxFeePerToken[address(safe)][feeToken]; | ||
if (fee > maxFee) revert FeeTooHigh(feeToken, fee); | ||
if (feeToken == NATIVE_TOKEN || feeToken == address(0)) { | ||
// If the native token is used for fee payment, then we directly send the fees to the fee collector | ||
actions[0].to = payable(feeCollector); | ||
actions[0].value = fee; | ||
actions[0].data = ""; | ||
} else { | ||
// If a ERC20 token is used for fee payment, then we trigger a token transfer on the token for the fee to the fee collector | ||
actions[0].to = payable(feeToken); | ||
actions[0].value = 0; | ||
actions[0].data = abi.encodeWithSignature("transfer(address,uint256)", feeCollector, fee); | ||
} | ||
// Note: Metadata format has not been proposed | ||
SafeTransaction memory safeTx = SafeTransaction({actions: actions, nonce: nonce, metadataHash: bytes32(0)}); | ||
try manager.executeTransaction(safe, safeTx) returns (bytes[] memory) {} catch (bytes memory reason) { | ||
revert FeePaymentFailure(reason); | ||
} | ||
} | ||
} | ||
|
||
contract SamplePlugin is BasePlugin { | ||
ISafeProtocolManager public immutable manager; | ||
function relayCall(address relayTarget, bytes calldata relayData) internal { | ||
// Check relay data to avoid that module can be abused for arbitrary interactions | ||
if (bytes4(relayData[:4]) != relayMethod) revert InvalidRelayMethod(bytes4(relayData[:4])); | ||
|
||
constructor( | ||
ISafeProtocolManager _manager | ||
) BasePlugin(PluginMetadata({name: "Sample Plugin", version: "1.0.0", requiresRootAccess: false, iconUrl: "", appUrl: ""})) { | ||
manager = _manager; | ||
// Perform relay call and require success to avoid that user paid for failed transaction | ||
(bool success, bytes memory data) = relayTarget.call(relayData); | ||
if (!success) revert RelayExecutionFailure(data); | ||
} | ||
|
||
function executeFromPlugin(ISafe safe, SafeTransaction calldata safetx) external returns (bytes[] memory data) { | ||
(data) = manager.executeTransaction(safe, safetx); | ||
function executeFromPlugin(ISafeProtocolManager manager, ISafe safe, bytes calldata data) external { | ||
if (trustedOrigin != address(0) && msg.sender != trustedOrigin) revert UntrustedOrigin(msg.sender); | ||
|
||
relayCall(address(safe), data); | ||
// We use the hash of the tx to relay has a nonce as this is unique | ||
uint256 nonce = uint256(keccak256(abi.encode(this, manager, safe, data, block.number))); | ||
payFee(manager, safe, nonce); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.