Skip to content

Commit

Permalink
Implemented Atomic Batch Redeem Delegation (#426)
Browse files Browse the repository at this point in the history
Co-authored-by: hanzel98 <[email protected]>
Co-authored-by: Dylan DesRosier <[email protected]>
  • Loading branch information
3 people authored Aug 18, 2024
1 parent 21ea8d3 commit 4ba5e33
Show file tree
Hide file tree
Showing 25 changed files with 626 additions and 543 deletions.
348 changes: 0 additions & 348 deletions .gas-snapshot

This file was deleted.

4 changes: 2 additions & 2 deletions documents/DelegationManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A Delegation Manager is responsible for validating delegations and triggering th

## Rules

- A Delegation Manager MUST implement `redeemDelegation` interface as specified `function redeemDelegation(bytes calldata _data, Action calldata _action) external;`.
- A Delegation Manager MUST implement `redeemDelegation` interface as specified `function redeemDelegation(bytes[] calldata _permissionContexts, Action[] calldata _actions) external;`.

## Delegations

Expand Down Expand Up @@ -34,7 +34,7 @@ Open delegations are delegations that don't have a strict `delegate`. By setting

Our `DelegationManager` implementation:

1. `redeemDelegation` consumes a list of delegations (`Delegation[]`) and an `Action` to be executed
1. `redeemDelegation` consumes an array of bytes with the encoded delegation chains (`Delegation[]`) for executing each of the `Action`.
> NOTE: Delegations are ordered from leaf to root. The last delegation in the array must have the root authority.
2. Validates the `msg.sender` calling `redeemDelegation` is allowed to do so
3. Validates the signatures of offchain delegations.
Expand Down
6 changes: 1 addition & 5 deletions script/DeployEnvironmentSetUp.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { LimitedCallsEnforcer } from "../src/enforcers/LimitedCallsEnforcer.sol"
import { NonceEnforcer } from "../src/enforcers/NonceEnforcer.sol";
import { TimestampEnforcer } from "../src/enforcers/TimestampEnforcer.sol";
import { ValueLteEnforcer } from "../src/enforcers/ValueLteEnforcer.sol";
import { NativeTokenTransferAmountEnforcer } from "../src/enforcers/NativeTokenTransferAmountEnforcer.sol";
import { NativeBalanceGteEnforcer } from "../src/enforcers/NativeBalanceGteEnforcer.sol";
import { NativeTokenPaymentEnforcer } from "../src/enforcers/NativeTokenPaymentEnforcer.sol";
import { ArgsEqualityCheckEnforcer } from "../src/enforcers/ArgsEqualityCheckEnforcer.sol";
Expand Down Expand Up @@ -108,13 +107,10 @@ contract DeployEnvironmentSetUp is Script {

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

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

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

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

Expand Down
15 changes: 7 additions & 8 deletions src/DeleGatorCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,14 @@ abstract contract DeleGatorCore is Initializable, UUPSUpgradeable, IERC165, IDel
receive() external payable { }

/**
* @notice Redeems a delegation on the DelegationManager and executes the action as the root delegator
* @dev `_data` is made up of an array of `Delegation` structs that are used to validate the authority given to execute the
* action
* on the root delegator ordered from leaf to root.
* @param _data the data used to validate the authority given to execute the action
* @param _action The action to be executed
* @notice Redeems a delegation on the DelegationManager and executes the specified actions on behalf of the root delegator.
* @param _permissionContexts An array of bytes where each element is made up of an array
* of `Delegation` structs that are used to validate the authority given to execute the corresponding action on the
* root delegator, ordered from leaf to root.
* @param _actions An array of `Action` structs representing the actions to be executed.
*/
function redeemDelegation(bytes calldata _data, Action calldata _action) external onlyEntryPointOrSelf {
delegationManager.redeemDelegation(_data, _action);
function redeemDelegation(bytes[] calldata _permissionContexts, Action[] calldata _actions) external onlyEntryPointOrSelf {
delegationManager.redeemDelegation(_permissionContexts, _actions);
}

/// @inheritdoc IDeleGatorCore
Expand Down
222 changes: 136 additions & 86 deletions src/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -108,112 +108,162 @@ contract DelegationManager is IDelegationManager, Ownable2Step, Pausable, EIP712
}

/**
* @notice This method validates the provided data and executes the action if the caller has authority to do so.
* @dev _data is made up of an array of `Delegation` structs that are used to validate the authority given to execute the action
* on the root delegator ordered from leaf to root.
* @dev Delegations can be invalidated for a number of reasons at any moment including invalid signatures due to ownership
* changes, disabled delegations or arbitrary caveat
* restrictions. Be sure to validate offchain before submitting anything onchain.
* @dev Delegations are ordered from leaf to root. The last delegation in the array must have the root authority.
* @dev Delegations are signed by the delegator and are validated at the time of redemption.
* @dev Caveats are enforced with the order: `beforeHook` from leaf to root, execute action, `afterHook` from root to leaf
* @param _data the data used to validate the authority given to execute the action.
* @param _action the action to be executed
* @notice This method validates the provided permission contexts and executes the action if the caller has authority to do so.
* @dev The structure of the _permissionContexts array is determined by the specific Delegation Manager implementation
* If an entry in _permissionsContexts is empty (i.e., its length is 0), it is treated as a self-authorized action.
* @dev The length of _permissionsContexts must match the length of _actions.
* @dev The afterHook calls of all the caveat enforcers are called after the execution of all the actions in the batch.
* @dev If any afterHook fails, the entire transaction will revert.
* @param _permissionContexts An array of bytes where each element is made up of an array
* of `Delegation` structs that are used to validate the authority given to execute the corresponding action on the
* root delegator, ordered from leaf to root.
* @param _actions the array of actions to be executed
*/
function redeemDelegation(bytes calldata _data, Action calldata _action) external whenNotPaused {
Delegation[] memory delegations_ = abi.decode(_data, (Delegation[]));
function redeemDelegation(bytes[] calldata _permissionContexts, Action[] calldata _actions) external whenNotPaused {
uint256 batchSize_ = _permissionContexts.length;
if (batchSize_ != _actions.length) revert BatchDataLengthMismatch();

// Validate caller
if (delegations_.length == 0) {
revert NoDelegationsProvided();
}

// Load delegation hashes and validate signatures (leaf to root)
bytes32[] memory delegationHashes_ = new bytes32[](delegations_.length);
Delegation memory delegation_;
Delegation[][] memory batchDelegations_ = new Delegation[][](batchSize_);
bytes32[][] memory batchDelegationHashes_ = new bytes32[][](batchSize_);

// Validate caller
if (delegations_[0].delegate != msg.sender && delegations_[0].delegate != ANY_DELEGATE) {
revert InvalidDelegate();
}
// Validate and process delegations for each action
for (uint256 batchIndex_; batchIndex_ < batchSize_; ++batchIndex_) {
Delegation[] memory delegations_ = abi.decode(_permissionContexts[batchIndex_], (Delegation[]));

for (uint256 i; i < delegations_.length; ++i) {
delegation_ = delegations_[i];
delegationHashes_[i] = EncoderLib._getDelegationHash(delegation_);

if (delegation_.signature.length == 0) {
// Ensure that delegations without signatures revert
revert EmptySignature();
}
// Check if the delegator is an EOA or a contract
address delegator_ = delegation_.delegator;

if (delegator_.code.length == 0) {
// Validate delegation if it's an EOA
address result_ =
ECDSA.recover(MessageHashUtils.toTypedDataHash(getDomainHash(), delegationHashes_[i]), delegation_.signature);
if (result_ != delegator_) revert InvalidSignature();
if (delegations_.length == 0) {
// Special case: If the permissionContext is empty, treat it as a self authorized action
batchDelegations_[batchIndex_] = new Delegation[](0);
batchDelegationHashes_[batchIndex_] = new bytes32[](0);
} else {
// Validate delegation if it's a contract
bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(getDomainHash(), delegationHashes_[i]);
batchDelegations_[batchIndex_] = delegations_;

// Load delegation hashes and validate signatures (leaf to root)
bytes32[] memory delegationHashes_ = new bytes32[](delegations_.length);
batchDelegationHashes_[batchIndex_] = delegationHashes_;

// Validate caller
if (delegations_[0].delegate != msg.sender && delegations_[0].delegate != ANY_DELEGATE) revert InvalidDelegate();

for (uint256 delegationsIndex_; delegationsIndex_ < delegations_.length; ++delegationsIndex_) {
Delegation memory delegation_ = delegations_[delegationsIndex_];
delegationHashes_[delegationsIndex_] = EncoderLib._getDelegationHash(delegation_);

if (delegation_.signature.length == 0) {
// Ensure that delegations without signatures revert
revert EmptySignature();
}

// Check if the delegator is an EOA or a contract
address delegator_ = delegation_.delegator;

if (delegator_.code.length == 0) {
// Validate delegation if it's an EOA
address result_ = ECDSA.recover(
MessageHashUtils.toTypedDataHash(getDomainHash(), delegationHashes_[delegationsIndex_]),
delegation_.signature
);
if (result_ != delegator_) revert InvalidSignature();
} else {
// Validate delegation if it's a contract
bytes32 typedDataHash_ =
MessageHashUtils.toTypedDataHash(getDomainHash(), delegationHashes_[delegationsIndex_]);

bytes32 result_ = IERC1271(delegator_).isValidSignature(typedDataHash_, delegation_.signature);
if (result_ != ERC1271Lib.EIP1271_MAGIC_VALUE) {
revert InvalidSignature();
}
}
}

bytes32 result_ = IERC1271(delegator_).isValidSignature(typedDataHash_, delegation_.signature);
if (result_ != ERC1271Lib.EIP1271_MAGIC_VALUE) {
revert InvalidSignature();
// Validate authority and delegate (leaf to root)
for (uint256 delegationsIndex_; delegationsIndex_ < delegations_.length; ++delegationsIndex_) {
// Validate if delegation is disabled
if (disabledDelegations[delegationHashes_[delegationsIndex_]]) {
revert CannotUseADisabledDelegation();
}

// Validate authority
if (delegationsIndex_ != delegations_.length - 1) {
if (delegations_[delegationsIndex_].authority != delegationHashes_[delegationsIndex_ + 1]) {
revert InvalidAuthority();
}
// Validate delegate
address nextDelegate_ = delegations_[delegationsIndex_ + 1].delegate;
if (nextDelegate_ != ANY_DELEGATE && delegations_[delegationsIndex_].delegator != nextDelegate_) {
revert InvalidDelegate();
}
} else if (delegations_[delegationsIndex_].authority != ROOT_AUTHORITY) {
revert InvalidAuthority();
}
}
}
}

// (leaf to root)
for (uint256 i; i < delegations_.length; ++i) {
// Validate if delegation is disabled
if (disabledDelegations[delegationHashes_[i]]) {
revert CannotUseADisabledDelegation();
}

// Validate authority
if (i != delegations_.length - 1) {
if (delegations_[i].authority != delegationHashes_[i + 1]) {
revert InvalidAuthority();
}
// Validate delegate
address nextDelegate = delegations_[i + 1].delegate;
if (nextDelegate != ANY_DELEGATE && delegations_[i].delegator != nextDelegate) {
revert InvalidDelegate();
// beforeHook (leaf to root)
for (uint256 batchIndex_; batchIndex_ < batchSize_; ++batchIndex_) {
if (batchDelegations_[batchIndex_].length > 0) {
Delegation[] memory delegations_ = batchDelegations_[batchIndex_];
bytes32[] memory delegationHashes_ = batchDelegationHashes_[batchIndex_];
// Execute beforeHooks
for (uint256 delegationsIndex_; delegationsIndex_ < delegations_.length; ++delegationsIndex_) {
Caveat[] memory caveats_ = delegations_[delegationsIndex_].caveats;
for (uint256 caveatsIndex_; caveatsIndex_ < caveats_.length; ++caveatsIndex_) {
ICaveatEnforcer enforcer_ = ICaveatEnforcer(caveats_[caveatsIndex_].enforcer);
enforcer_.beforeHook(
caveats_[caveatsIndex_].terms,
caveats_[caveatsIndex_].args,
_actions[batchIndex_],
delegationHashes_[delegationsIndex_],
delegations_[delegationsIndex_].delegator,
msg.sender
);
}
}
} else if (delegations_[i].authority != ROOT_AUTHORITY) {
revert InvalidAuthority();
}
}

// beforeHook (leaf to root)
for (uint256 i; i < delegations_.length; ++i) {
Caveat[] memory caveats_ = delegations_[i].caveats;
bytes32 delegationHash_ = delegationHashes_[i];
address delegator_ = delegations_[i].delegator;
uint256 caveatsLength_ = caveats_.length;
for (uint256 j; j < caveatsLength_; ++j) {
ICaveatEnforcer enforcer_ = ICaveatEnforcer(caveats_[j].enforcer);
enforcer_.beforeHook(caveats_[j].terms, caveats_[j].args, _action, delegationHash_, delegator_, msg.sender);
for (uint256 batchIndex_; batchIndex_ < batchSize_; ++batchIndex_) {
if (batchDelegations_[batchIndex_].length == 0) {
// special case: If there are no delegations, defer the call to the caller.
IDeleGatorCore(msg.sender).executeDelegatedAction(_actions[batchIndex_]);
} else {
IDeleGatorCore(batchDelegations_[batchIndex_][batchDelegations_[batchIndex_].length - 1].delegator)
.executeDelegatedAction(_actions[batchIndex_]);
}
}

// Execute action (root)
IDeleGatorCore(delegations_[delegations_.length - 1].delegator).executeDelegatedAction(_action);

// afterHook (root to leaf)
for (uint256 i = delegations_.length; i > 0; --i) {
Caveat[] memory caveats_ = delegations_[i - 1].caveats;
bytes32 delegationHash_ = delegationHashes_[i - 1];
address delegator_ = delegations_[i - 1].delegator;
uint256 caveatsLength_ = caveats_.length;
for (uint256 j; j < caveatsLength_; ++j) {
ICaveatEnforcer enforcer_ = ICaveatEnforcer(caveats_[j].enforcer);
enforcer_.afterHook(caveats_[j].terms, caveats_[j].args, _action, delegationHash_, delegator_, msg.sender);
for (uint256 batchIndex_; batchIndex_ < batchSize_; ++batchIndex_) {
if (batchDelegations_[batchIndex_].length > 0) {
Delegation[] memory delegations_ = batchDelegations_[batchIndex_];
bytes32[] memory delegationHashes_ = batchDelegationHashes_[batchIndex_];
// Execute afterHooks
for (uint256 delegationsIndex_ = delegations_.length; delegationsIndex_ > 0; --delegationsIndex_) {
Caveat[] memory caveats_ = delegations_[delegationsIndex_ - 1].caveats;
for (uint256 caveatsIndex_; caveatsIndex_ < caveats_.length; ++caveatsIndex_) {
ICaveatEnforcer enforcer_ = ICaveatEnforcer(caveats_[caveatsIndex_].enforcer);
enforcer_.afterHook(
caveats_[caveatsIndex_].terms,
caveats_[caveatsIndex_].args,
_actions[batchIndex_],
delegationHashes_[delegationsIndex_ - 1],
delegations_[delegationsIndex_ - 1].delegator,
msg.sender
);
}
}
}
}
for (uint256 i; i < delegations_.length; ++i) {
emit RedeemedDelegation(delegations_[delegations_.length - 1].delegator, msg.sender, delegations_[i]);

for (uint256 batchIndex_; batchIndex_ < batchSize_; ++batchIndex_) {
if (batchDelegations_[batchIndex_].length > 0) {
Delegation[] memory delegations_ = batchDelegations_[batchIndex_];
for (uint256 delegationsIndex_; delegationsIndex_ < delegations_.length; ++delegationsIndex_) {
emit RedeemedDelegation(
delegations_[delegations_.length - 1].delegator, msg.sender, delegations_[delegationsIndex_]
);
}
}
}
}

Expand Down
10 changes: 7 additions & 3 deletions src/enforcers/NativeTokenPaymentEnforcer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,16 @@ contract NativeTokenPaymentEnforcer is CaveatEnforcer {
}
}

bytes memory newEncodedContext_ = abi.encode(delegations_);

uint256 balanceBefore_ = recipient_.balance;

bytes[] memory encodedDelegations_ = new bytes[](1);
encodedDelegations_[0] = abi.encode(delegations_);

Action[] memory actions_ = new Action[](1);
actions_[0] = Action({ to: recipient_, value: amount_, data: hex"" });

// Attempt to redeem the delegation and make the payment
delegationManager.redeemDelegation(newEncodedContext_, Action({ to: recipient_, value: amount_, data: hex"" }));
delegationManager.redeemDelegation(encodedDelegations_, actions_);

// Ensure the recipient received the payment
uint256 balanceAfter_ = recipient_.balance;
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/IDeleGatorCoreFull.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface IDeleGatorCoreFull is IDeleGatorCore, IERC165 {

////////////////////////////// MM Implementation Methods //////////////////////////////

function redeemDelegation(bytes calldata _data, Action calldata _action) external;
function redeemDelegation(bytes[] calldata _permissionContexts, Action[] calldata _actions) external;

function execute(Action calldata _action) external;

Expand Down
Loading

0 comments on commit 4ba5e33

Please sign in to comment.