Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
PRIVATE_KEY=
SALT=GATOR
DELEGATION_MANAGER_ADDRESS=
META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS=
ENTRYPOINT_ADDRESS=0x0000000071727De22E5E9d8BAf0edAc6f37da032
MULTISIG_DELEGATOR_IMPLEMENTATION_ADDRESS=
META_SWAP_ADAPTER_OWNER_ADDRESS=
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
### Getting Started

1. **Fork the repository**:

- Click the "Fork" button at the top right of the repository page.

2. **Clone your fork**:
Expand Down Expand Up @@ -66,6 +65,12 @@ The Delegation Manager includes the logic for validating and executing Delegatio

[Read more on "Delegation Manager" ->](/documents/DelegationManager.md)

### MetaSwap Order Delegation Manager

One manager for two flows: an exact batch, and a MetaSwap limit order whose route is chosen at redemption inside signed bounds.

[Read more on "MetaSwap Order Delegation Manager" ->](/documents/MetaSwapSpecializedDelegationManagers.md)

### Caveat Enforcers

Caveats are used to add restrictions and rules for Delegations. By default, a Delegation allows the delegate to make **any** onchain action so caveats are strongly recommended. They are managed by Caveat Enforcer contracts.
Expand Down
41 changes: 41 additions & 0 deletions documents/MetaSwapSpecializedDelegationManagers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# MetaSwap Order Delegation Manager

One delegation manager for two MetaSwap flows. It redeems a single root delegation, in batch mode, with one caveat whose enforcer is the manager itself. There is no redelegation chain and no external caveat hook.

`executeFromExecutor` stays on the EIP-7702 account. An account that uses this manager cannot also redeem through `DelegationManager`.

## Exact batch

The user signs the hash of a batch. The submitter must send that same batch. Expiry stays off-chain.

```text
terms = intent(1) | executionHash(32)
executionCallDatas[0] = ExecutionLib.encodeBatch(executions)
```

`executionHash` is `keccak256(executionCallDatas[0])`. The batch is executed directly. `delegate` is the redeemer, or `ANY_DELEGATE`. `disabledDelegations` is the one-shot.

## Limit order

The user signs the settlement bounds. The redeemer supplies the batch and chooses the route. The manager checks the approval and swap shape, not the aggregator id or the route data, then requires `tokenOutMin` to arrive at the signed recipient.

```text
terms = intent(1) | metaSwap(20) | tokenIn(20) | tokenInAmount(32) | approvalMode(1)
| tokenOut(20) | recipient(20) | tokenOutMin(32)
| timestampAfter(16) | timestampBefore(16) | id(32) | redeemers(20*N)
```

- Redeemers are required (`N >= 1`). `address(0)` is a normal allowlist entry for Sentinel pre-sign simulation. A multi-signer order sets `delegate` to `ANY_DELEGATE` and lists the stations.
- Timestamp bounds are optional. `0` disables that bound. A set bound is exclusive (`timestamp > after` and `timestamp < before`).
- `id == 0` skips the bitmap. The delegation hash is still one-shot. `id != 0` also burns that id for the delegator, so a sibling order that shares it cannot fill. `disableDelegation` does not burn the id.

Approval shapes are native swap, skip approval, approve then swap, and reset-approve then swap.

Signatures try ECDSA first. If that misses, an account with no code reverts. An account with code falls back to ERC-1271. `disabledDelegations` is both cancel and one-shot consumption. A failed execution or insufficient output reverts atomically.

## Limitations

- No delegation chains, multiple caveats, multiple batches, try mode, or generic enforcers.
- No on-chain expiry for an exact batch. A limit order can set a timestamp window.
- No `enableDelegation` and no pause.
- A listed redeemer can choose any route that still pays the signed minimum to the signed recipient.
39 changes: 39 additions & 0 deletions script/DeployMetaSwapOrderDelegationManager.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import "forge-std/Script.sol";
import { console2 } from "forge-std/console2.sol";

import { MetaSwapOrderDelegationManager } from "../src/MetaSwapOrderDelegationManager.sol";

/**
* @title DeployMetaSwapOrderDelegationManager.s.sol
* @notice Deploys MetaSwapOrderDelegationManager.
* @dev EIP-7702 accounts that redeem through this manager must use it as their delegation manager.
* @dev run the script with:
* forge script script/DeployMetaSwapOrderDelegationManager.s.sol --rpc-url <your_rpc_url> --private-key $PRIVATE_KEY --broadcast
*/
contract DeployMetaSwapOrderDelegationManager is Script {
bytes32 salt;
address deployer;

function setUp() public {
salt = bytes32(abi.encodePacked(vm.envString("SALT")));
deployer = msg.sender;

console2.log("~~~");
console2.log("Deployer: %s", deployer);
console2.log("Salt:");
console2.logBytes32(salt);
}

function run() public {
console2.log("~~~");
vm.startBroadcast();

address deployedAddress = address(new MetaSwapOrderDelegationManager{ salt: salt }());
console2.log("MetaSwapOrderDelegationManager: %s", deployedAddress);

vm.stopBroadcast();
}
}
11 changes: 11 additions & 0 deletions script/verification/VerificationInstructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ Verifies an array of enforcer contracts.
./verify-enforcer-contracts.sh
```

#### `verify-metaswap-order-delegation-manager.sh`

Verifies `MetaSwapOrderDelegationManager`. The constructor takes no arguments.
Requires `META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS`.

**Usage:**

```bash
./verify-metaswap-order-delegation-manager.sh
```

## Notes

- Ensure the `.env` file is correctly configured and contains all necessary API keys.
Expand Down
78 changes: 78 additions & 0 deletions script/verification/verify-metaswap-order-delegation-manager.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# verify-metaswap-order-delegation-manager.sh
#
# Usage:
# ./verify-metaswap-order-delegation-manager.sh
#
# Verifies MetaSwapOrderDelegationManager across configured chains.
# The constructor takes no arguments.
# Requires in .env:
# META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS

set -e

# Load environment variables
set -o allexport
source ../../.env
set +o allexport

# Load shared logic
source ./verify-utils.sh

##########################################
# Helper functions for contract configuration
##########################################

# Function to add a contract to verify
add_contract() {
local name="$1"
local path="$2"
local address="$3"
local constructor_args="$4"
local lib_string="$5"

# Add to contracts array
CONTRACTS+=("$name:$path:$address:$constructor_args:$lib_string")
}

##########################################
# Contract Configurations
##########################################

# Initialize empty array
declare -a CONTRACTS

# MetaSwapOrderDelegationManager
add_contract \
"MetaSwapOrderDelegationManager" \
"src/MetaSwapOrderDelegationManager.sol" \
"${META_SWAP_ORDER_DELEGATION_MANAGER_ADDRESS}" \
"" \
""

##########################################
# Process Contracts
##########################################

# Process each contract
for contract in "${CONTRACTS[@]}"; do
# Split the configuration string
IFS=':' read -r name path address constructor_args lib_string <<< "$contract"

echo "============================================="
echo "Verifying contract: $name"
echo "============================================="

# Call the shared function
verify_across_chains \
"$path" \
"$name" \
"$address" \
"$constructor_args" \
"$lib_string"

echo "============================================="
echo "Completed verification for: $name"
echo "============================================="
echo
done
170 changes: 170 additions & 0 deletions src/MetaSwapDelegationManagerBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: MIT AND Apache-2.0
pragma solidity 0.8.23;

import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

import { EncoderLib } from "./libraries/EncoderLib.sol";
import { ERC1271Lib } from "./libraries/ERC1271Lib.sol";
import { DELEGATION_TYPEHASH, CAVEAT_TYPEHASH } from "./utils/Constants.sol";
import { Caveat, Delegation, ModeCode } from "./utils/Types.sol";

/**
* @title MetaSwapDelegationManagerBase
* @notice Cheap one-shot redeem shell for purpose-specific MetaSwap managers.
* @dev Supports exactly one root delegation containing one manager-enforced caveat.
* No redelegation chains: `delegate` is the intended redeemer (or `ANY_DELEGATE`) and
* `authority` is `ROOT_AUTHORITY`. Redemption is `SIMPLE_BATCH_MODE` only.
* Settlement-specific decoding and min-output checks live in subclasses.
*/
abstract contract MetaSwapDelegationManagerBase is EIP712 {
string public constant DOMAIN_VERSION = "1";
bytes32 public constant ROOT_AUTHORITY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
address public constant ANY_DELEGATE = address(0xa11);
/// @dev Equivalent to `ModeLib.encodeSimpleBatch()` (batch calltype, default exec).
ModeCode public constant SIMPLE_BATCH_MODE = ModeCode.wrap(0x0100000000000000000000000000000000000000000000000000000000000000);

/// @notice Records delegations that were cancelled or successfully consumed.
mapping(bytes32 delegationHash => bool isUnavailable) public disabledDelegations;

event DisabledDelegation(
bytes32 indexed delegationHash, address indexed delegator, address indexed delegate, Delegation delegation
);
/// @dev `intent` is the first terms byte (`Intent` on MetaSwapOrderDelegationManager).
event RedeemedDelegation(address indexed rootDelegator, address indexed redeemer, bytes32 indexed delegationHash, uint8 intent);

error AlreadyDisabled();
error BatchDataLengthMismatch();
error CannotUseADisabledDelegation();
error InsufficientOutput();
error InvalidAuthority();
error InvalidCaveat();
error InvalidDelegate();
error InvalidDelegator();
error InvalidEOASignature();
error InvalidERC1271Signature();
error InvalidMode();
error InvalidPermissionContext();
error InvalidTerms();

constructor(string memory name_) EIP712(name_, DOMAIN_VERSION) { }

/**
* @notice Cancels a settlement delegation.
* @dev Successful settlements use the same state, so consumed delegations cannot be re-enabled.
* @param delegation_ Delegation to cancel.
*/
function disableDelegation(Delegation calldata delegation_) external {
if (delegation_.delegator != msg.sender) revert InvalidDelegator();

bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_);
if (disabledDelegations[delegationHash_]) revert AlreadyDisabled();

disabledDelegations[delegationHash_] = true;
emit DisabledDelegation(delegationHash_, delegation_.delegator, delegation_.delegate, delegation_);
}

/**
* @notice Redeems one specialized MetaSwap delegation.
* @dev These intents are leaf delegations: `delegate` is a specific redeemer (or `ANY_DELEGATE`),
* `authority` is always `ROOT_AUTHORITY` so there is no redelegation chain, and `modes_[0]`
* must be `SIMPLE_BATCH_MODE`.
* @param permissionContexts_ Must contain one ABI-encoded one-element `Delegation[]`.
* @param modes_ Must contain the canonical batch/default mode.
* @param executionContexts_ Manager-specific execution context.
*/
function redeemDelegations(
bytes[] calldata permissionContexts_,
ModeCode[] calldata modes_,
bytes[] calldata executionContexts_
)
external
{
if (permissionContexts_.length != 1 || modes_.length != 1 || executionContexts_.length != 1) {
revert BatchDataLengthMismatch();
}
if (ModeCode.unwrap(modes_[0]) != ModeCode.unwrap(SIMPLE_BATCH_MODE)) revert InvalidMode();

Delegation[] memory delegations_ = abi.decode(permissionContexts_[0], (Delegation[]));
if (delegations_.length != 1) revert InvalidPermissionContext();

Delegation memory delegation_ = delegations_[0];
if (delegation_.delegate != msg.sender && delegation_.delegate != ANY_DELEGATE) revert InvalidDelegate();
if (delegation_.authority != ROOT_AUTHORITY) revert InvalidAuthority();
if (delegation_.caveats.length != 1 || delegation_.caveats[0].enforcer != address(this)) revert InvalidCaveat();

bytes32 delegationHash_ = _getSingleCaveatDelegationHash(delegation_);
if (disabledDelegations[delegationHash_]) revert CannotUseADisabledDelegation();

_validateSignature(delegation_, delegationHash_);

disabledDelegations[delegationHash_] = true;
bytes memory terms_ = delegation_.caveats[0].terms;
_executeIntent(delegation_.delegator, terms_, executionContexts_[0]);

emit RedeemedDelegation(delegation_.delegator, msg.sender, delegationHash_, uint8(terms_[0]));
}

/**
* @notice Returns the EIP-712 hash used to sign a delegation.
* @param delegation_ Delegation to hash.
*/
function getDelegationHash(Delegation calldata delegation_) external pure returns (bytes32) {
return EncoderLib._getDelegationHash(delegation_);
}

/**
* @notice Returns this manager's EIP-712 domain separator.
*/
function getDomainHash() external view returns (bytes32) {
return _domainSeparatorV4();
}

/**
* @notice Executes the signed intent after the one-shot lock is recorded.
* @param delegator_ Root delegator account that will execute.
* @param terms_ Signed caveat terms.
* @param executionContext_ Redeemer-supplied execution context.
*/
function _executeIntent(address delegator_, bytes memory terms_, bytes calldata executionContext_) internal virtual;

function _validateSignature(Delegation memory delegation_, bytes32 delegationHash_) private view {
bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), delegationHash_);
(address recovered_, ECDSA.RecoverError error_,) = ECDSA.tryRecover(typedDataHash_, delegation_.signature);
if (error_ == ECDSA.RecoverError.NoError && recovered_ == delegation_.delegator) return;

// Codeless delegators are EOAs: a non-matching recovery cannot succeed via ERC-1271.
if (delegation_.delegator.code.length == 0) revert InvalidEOASignature();

if (
IERC1271(delegation_.delegator).isValidSignature(typedDataHash_, delegation_.signature)
!= ERC1271Lib.EIP1271_MAGIC_VALUE
) {
revert InvalidERC1271Signature();
}
}

function _getSingleCaveatDelegationHash(Delegation memory delegation_) internal pure returns (bytes32) {
Caveat memory caveat_ = delegation_.caveats[0];
bytes32 caveatHash_ = keccak256(abi.encode(CAVEAT_TYPEHASH, caveat_.enforcer, keccak256(caveat_.terms)));
bytes32 caveatsHash_ = keccak256(abi.encodePacked(caveatHash_));

return keccak256(
abi.encode(
DELEGATION_TYPEHASH,
delegation_.delegate,
delegation_.delegator,
delegation_.authority,
caveatsHash_,
delegation_.salt
)
);
}

function _balanceOf(address token_, address recipient_) internal view returns (uint256) {
return token_ == address(0) ? recipient_.balance : IERC20(token_).balanceOf(recipient_);
}
}
Loading
Loading