Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make settling swaps on SettlerIntent permissioned #293

Open
wants to merge 35 commits into
base: dcmt/multicall-2771
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1d7f292
WIP: Make settling swaps on `SettlerIntent` permissioned
duncancmt Jan 28, 2025
9306e4b
Bug! Bad swap-and-pop in `SettlerIntent`
duncancmt Jan 28, 2025
c01e099
Pedantically, run the permission check before setting the metatx para…
duncancmt Jan 28, 2025
19576de
Merge branch 'master' into dcmt/intent-permissioned
duncancmt Jan 29, 2025
40a2cda
Golf
duncancmt Jan 29, 2025
70475ab
Golf
duncancmt Jan 29, 2025
988c283
Bug hunt
duncancmt Jan 29, 2025
19e91f4
Golf
duncancmt Jan 29, 2025
d18f6ff
Cleanup
duncancmt Jan 29, 2025
beeef86
Comments
duncancmt Jan 30, 2025
fc46136
Give an explicit revert reason in `setSolver`
duncancmt Jan 30, 2025
c0883d3
Comment
duncancmt Jan 30, 2025
1dd7eec
Form the `_SOLVER_LIST_BASE_SLOT` in a ERC1976-ish fashion
duncancmt Jan 30, 2025
bd8472c
Comment
duncancmt Jan 30, 2025
98ee81c
Typo
duncancmt Jan 30, 2025
9a18dd5
Remove event because we can use storage to enumerate the state change…
duncancmt Jan 30, 2025
23d5583
Comment
duncancmt Jan 30, 2025
90c5702
Simplify
duncancmt Jan 30, 2025
26f9245
DRY
duncancmt Jan 30, 2025
50db50c
Formatting
duncancmt Jan 30, 2025
41f2f00
Pedantry
duncancmt Jan 30, 2025
07d3527
Pedantry
duncancmt Jan 30, 2025
faa539e
Always clean dirty bits
duncancmt Feb 6, 2025
a33c023
Merge branch 'dcmt/multicall-2771' into dcmt/intent-permissioned
duncancmt Feb 10, 2025
11811a6
Use `MultiCallContext` in `SettlerIntent`
duncancmt Feb 10, 2025
1cd1c5d
`forge fmt`
duncancmt Feb 10, 2025
70404d8
Golf
duncancmt Feb 10, 2025
04ef79d
Bug! OOM on failure on `safeApprove` and `safeTransfer`
duncancmt Feb 10, 2025
9ba47f8
Golf `DodoV1` like my life depends on it
duncancmt Feb 10, 2025
e2a1192
Add checks for short `returndata`
duncancmt Feb 10, 2025
6eaca8e
Fix bugs
duncancmt Feb 10, 2025
3ba22b7
Make the list of authorized solvers enumerable through an accessor
duncancmt Feb 11, 2025
7328c01
Check for dirty `returndata`
duncancmt Feb 11, 2025
6901600
Bug! Use the correct ancestor contract implementation of `_msgSender(…
duncancmt Feb 11, 2025
5ac079e
Merge branch 'dcmt/multicall-2771' into dcmt/intent-permissioned
duncancmt Feb 11, 2025
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
44 changes: 21 additions & 23 deletions src/Settler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {Permit2PaymentTakerSubmitted} from "./core/Permit2Payment.sol";
import {Permit2PaymentAbstract} from "./core/Permit2PaymentAbstract.sol";

import {AbstractContext} from "./Context.sol";
import {AllowanceHolderContext} from "./allowanceholder/AllowanceHolderContext.sol";
import {CalldataDecoder, SettlerBase} from "./SettlerBase.sol";
import {UnsafeMath} from "./utils/UnsafeMath.sol";

Expand All @@ -26,28 +25,6 @@ abstract contract Settler is Permit2PaymentTakerSubmitted, SettlerBase {
return false;
}

function _msgSender()
internal
view
virtual
// Solidity inheritance is so stupid
override(Permit2PaymentTakerSubmitted, AbstractContext)
returns (address)
{
return super._msgSender();
}

function _isRestrictedTarget(address target)
internal
pure
virtual
// Solidity inheritance is so stupid
override(Permit2PaymentTakerSubmitted, Permit2PaymentAbstract)
returns (bool)
{
return super._isRestrictedTarget(target);
}

function _dispatchVIP(uint256 action, bytes calldata data) internal virtual returns (bool) {
if (action == uint32(ISettlerActions.TRANSFER_FROM.selector)) {
(address recipient, ISignatureTransfer.PermitTransferFrom memory permit, bytes memory sig) =
Expand Down Expand Up @@ -117,4 +94,25 @@ abstract contract Settler is Permit2PaymentTakerSubmitted, SettlerBase {
_checkSlippageAndTransfer(slippage);
return true;
}

// Solidity inheritance is stupid
function _msgSender()
internal
view
virtual
override(Permit2PaymentTakerSubmitted, AbstractContext)
returns (address)
{
return super._msgSender();
}

function _isRestrictedTarget(address target)
internal
pure
virtual
override(Permit2PaymentTakerSubmitted, Permit2PaymentAbstract)
returns (bool)
{
return super._isRestrictedTarget(target);
}
}
6 changes: 3 additions & 3 deletions src/SettlerBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ library CalldataDecoder {
data.offset,
// We allow the indirection/offset to `calls[i]` to be negative
calldataload(
add(shl(5, i), data.offset) // can't overflow; we assume `i` is in-bounds
add(shl(0x05, i), data.offset) // can't overflow; we assume `i` is in-bounds
)
)
// now we load `args.length` and set `args.offset` to the start of data
args.length := calldataload(args.offset)
args.offset := add(args.offset, 0x20)
args.offset := add(0x20, args.offset)

// slice off the first 4 bytes of `args` as the selector
selector := shr(0xe0, calldataload(args.offset))
args.length := sub(args.length, 0x04)
args.offset := add(args.offset, 0x04)
args.offset := add(0x04, args.offset)
}
}
}
Expand Down
249 changes: 227 additions & 22 deletions src/SettlerIntent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,203 @@ import {SettlerMetaTxn} from "./SettlerMetaTxn.sol";
import {Permit2PaymentAbstract} from "./core/Permit2PaymentAbstract.sol";
import {Permit2PaymentIntent, Permit2PaymentMetaTxn, Permit2Payment} from "./core/Permit2Payment.sol";

import {AbstractContext, Context} from "./Context.sol";
import {MultiCallContext} from "./multicall/MultiCallContext.sol";

import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol";

abstract contract SettlerIntent is Permit2PaymentIntent, SettlerMetaTxn {
function _tokenId() internal pure virtual override(SettlerAbstract, SettlerMetaTxn) returns (uint256) {
return 4;
import {DEPLOYER} from "./deployer/DeployerAddress.sol";
import {IDeployer} from "./deployer/IDeployer.sol";
import {Feature} from "./deployer/Feature.sol";
import {IOwnable} from "./deployer/IOwnable.sol";

abstract contract SettlerIntent is Permit2PaymentIntent, SettlerMetaTxn, MultiCallContext {
bytes32 private constant _SOLVER_LIST_BASE_SLOT = 0x00000000000000000000000000000000e4441b0608054751d605e5c08a2210bf; // uint128(uint256(keccak256("SettlerIntentSolverList")) - 1)
bytes32 private constant _SOLVER_LIST_START_SLOT =
0x165458a486c543a8294bbc8a8476cd9020f962f9e80991591ef8c2860c5c5490; // keccak256(abi.encode(_SENTINEL_SOLVER, _SOLVER_LIST_BASE_SLOT))

/// This mapping forms a circular singly-linked list that traverses all the authorized callers
/// of `executeMetaTxn`. The head and tail of the list is `address(1)`, which is the constant
/// `_SENTINEL_SOLVER`. No view function is provided for accessing this mapping. You'll have to
/// use an RPC to read storage directly and reconstruct the list that way. As a consequence of
/// the structure of this list, the check for whether an address is on the list is extremely
/// simple: `_$()[query] != address(0)`. This technique is cribbed from Safe{Wallet}
function _$() private pure returns (mapping(address => address) storage $) {
assembly ("memory-safe") {
$.slot := _SOLVER_LIST_BASE_SLOT
}
}

function _msgSender()
internal
view
virtual
// Solidity inheritance is so stupid
override(Permit2PaymentMetaTxn, SettlerMetaTxn)
returns (address)
{
return super._msgSender();
address private constant _SENTINEL_SOLVER = 0x0000000000000000000000000000000000000001;

constructor() {
assert(_SOLVER_LIST_BASE_SLOT == bytes32(uint256(uint128(uint256(keccak256("SettlerIntentSolverList")) - 1))));
assert(_SOLVER_LIST_START_SLOT == keccak256(abi.encode(_SENTINEL_SOLVER, _SOLVER_LIST_BASE_SLOT)));
_$()[_SENTINEL_SOLVER] = _SENTINEL_SOLVER;
}

function _witnessTypeSuffix()
internal
pure
virtual
// Solidity inheritance is so stupid
override(Permit2PaymentMetaTxn, Permit2PaymentIntent)
returns (string memory)
{
return super._witnessTypeSuffix();
modifier onlyOwner() {
// Solidity generates extremely bloated code for the following block, so it has been
// rewritten in assembly so as not to blow out the contract size limit
/*
(address owner, uint40 expiry) = IDeployer(DEPLOYER).authorized(Feature.wrap(uint128(_tokenId())));
*/
address deployer_ = DEPLOYER;
uint256 tokenId_ = _tokenId();
address owner;
uint40 expiry;
assembly ("memory-safe") {
// We lay out the calldata in memory in the first 2 slots. The first slot is the
// selector, but aligned incorrectly (this significantly saves on contract size). The
// second slot is the token ID. Therefore calldata starts at offset 0x1c (32 - 4) and is
// 0x24 bytes long (32 + 4)
mstore(0x00, 0x2bb83987) // selector for `authorized(uint128)`
mstore(0x20, tokenId_)

// Perform the call and bubble any revert. The expected returndata (2 arguments, each 1
// slot) is copied back into the first 2 slots of memory.
if iszero(staticcall(gas(), deployer_, 0x1c, 0x24, 0x00, 0x40)) {
let ptr := mload(0x40)
returndatacopy(ptr, 0x00, returndatasize())
revert(ptr, returndatasize())
}

// If calldata is short (we need at least 64 bytes), revert with an empty reason.
if iszero(gt(returndatasize(), 0x3f)) { revert(0x00, 0x00) }

// Load the return values that were automatically written into the first 2 slots of
// memory.
owner := mload(0x00)
expiry := mload(0x20)

// If there are any dirty bits in the return values, revert with an empty reason.
if or(shr(0xa0, owner), shr(0x28, expiry)) { revert(0x00, 0x00) }
}

// Check that the owner actually exists, that is that their authority hasn't expired.
require(expiry == type(uint40).max || block.timestamp <= expiry);

// Check that the caller (in this case `_operator()`, because we aren't using the special
// transient-storage taker logic) is the owner.
if (_operator() != owner) {
revert IOwnable.PermissionDenied();
}
_;
}

modifier onlySolver() {
if (_$()[_operator()] == address(0)) {
revert IOwnable.PermissionDenied();
}
_;
}

error InvalidSolver(address prev, address solver);

/// This pattern is cribbed from Safe{Wallet}. See `OwnerManager.sol` from
/// 0x3E5c63644E683549055b9Be8653de26E0B4CD36E.
function setSolver(address prev, address solver, bool addNotRemove) external onlyOwner {
// Solidity generates extremely bloated code for the following block, so it has been
// rewritten in assembly so as not to blow out the contract size limit
/*
require(solver != address(0));
mapping(address => address) storage $ = _$();
require(($[solver] == address(0)) == addNotRemove);
if (addNotRemove) {
require($[prev] == _SENTINEL_SOLVER);
$[prev] = solver;
$[solver] = _SENTINEL_SOLVER;
} else {
require($[prev] == solver);
$[prev] = $[solver];
$[solver] = address(0);
}
*/
assembly ("memory-safe") {
// Clean dirty bits.
prev := and(0xffffffffffffffffffffffffffffffffffffffff, prev)
solver := and(0xffffffffffffffffffffffffffffffffffffffff, solver)

// A solver of zero is special-cased. It is forbidden to set it because that would
// corrupt the list.
let fail := iszero(solver)

// Derive the slot for `solver` and load it.
mstore(0x00, solver)
mstore(0x20, _SOLVER_LIST_BASE_SLOT)
let solverSlot := keccak256(0x00, 0x40)
let solverSlotValue := and(0xffffffffffffffffffffffffffffffffffffffff, sload(solverSlot))

// If the slot contains zero, `addNotRemove` must be true (we are adding a new
// solver). Likewise if the slot contains nonzero, `addNotRemove` must be false (we are
// removing one).
fail := or(fail, xor(iszero(solverSlotValue), addNotRemove))

// Derive the slot for `prev`.
mstore(0x00, prev)
let prevSlot := keccak256(0x00, 0x40)

// This is a very fancy way of writing:
// expectedPrevSlotValue = addNotRemove ? _SENTINEL_SOLVER : solver
// newPrevSlotValue = addNotRemove ? solver : solverSlotValue
let expectedPrevSlotValue := xor(solver, mul(xor(_SENTINEL_SOLVER, solver), addNotRemove))
let newPrevSlotValue := xor(solverSlotValue, mul(xor(solverSlotValue, solver), addNotRemove))

// Check that the value for `prev` matches the value for `solver`. If we are adding a
// new solver, then `prev` must be the last element of the list (it points at
// `_SENTINEL_SOLVER`). If we are removing an existing solver, then `prev` must point at
// `solver.
fail :=
or(fail, xor(and(0xffffffffffffffffffffffffffffffffffffffff, sload(prevSlot)), expectedPrevSlotValue))

// Update the linked list. This either points `$[prev]` at `$[solver]` and zeroes
// `$[solver]` or it points `$[prev]` at `solver` and points `$[solver]` at
// `_SENTINEL_SOLVER`
sstore(prevSlot, newPrevSlotValue)
sstore(solverSlot, addNotRemove)

// If any of the checks failed, revert. This check is deferred because it makes the
// contract substantially smaller.
if fail {
mstore(0x00, 0xe2b339fd) // selector for `InvalidSolver(address,address)`
mstore(0x20, prev)
mstore(0x40, solver)
revert(0x1c, 0x44)
}
}
}

/// This function is not intended to be called on-chain. It's only for being `eth_call`'d. There
/// is a somewhat obvious DoS vector here if called on-chain, so just don't do that.
function getSolvers() external view returns (address[] memory) {
assembly ("memory-safe") {
let ptr := mload(0x40)

let len
{
let start := add(0x40, ptr)
let i := start
for {
mstore(0x20, _SOLVER_LIST_BASE_SLOT)
let x := and(0xffffffffffffffffffffffffffffffffffffffff, sload(_SOLVER_LIST_START_SLOT))
} xor(x, _SENTINEL_SOLVER) {
i := add(0x20, i)
x := and(0xffffffffffffffffffffffffffffffffffffffff, sload(keccak256(0x00, 0x40)))
} {
mstore(i, x)
mstore(0x00, x)
}
len := sub(i, start)
}

mstore(ptr, 0x20)
mstore(add(0x20, ptr), shr(0x05, len))
return(ptr, add(0x40, len))
}
}

function _tokenId() internal pure virtual override(SettlerAbstract, SettlerMetaTxn) returns (uint256) {
return 4;
}

function _mandatorySlippageCheck() internal pure virtual override returns (bool) {
Expand All @@ -60,7 +230,7 @@ abstract contract SettlerIntent is Permit2PaymentIntent, SettlerMetaTxn {
bytes32, /* zid & affiliate */
address msgSender,
bytes calldata sig
) public virtual override metaTx(msgSender, _hashSlippage(slippage)) returns (bool) {
) public virtual override onlySolver metaTx(msgSender, _hashSlippage(slippage)) returns (bool) {
return _executeMetaTxn(slippage, actions, sig);
}

Expand All @@ -73,4 +243,39 @@ abstract contract SettlerIntent is Permit2PaymentIntent, SettlerMetaTxn {
{
sellAmount = permit.permitted.amount;
}

function _isForwarded() internal view virtual override(AbstractContext, Context, MultiCallContext) returns (bool) {
return Context._isForwarded(); // false
}

function _msgSender()
internal
view
virtual
override(Permit2PaymentMetaTxn, SettlerMetaTxn, MultiCallContext)
returns (address)
{
return Permit2PaymentMetaTxn._msgSender();
}

// Solidity inheritance is stupid
function _msgData()
internal
view
virtual
override(AbstractContext, Context, MultiCallContext)
returns (bytes calldata)
{
return super._msgData();
}

function _witnessTypeSuffix()
internal
pure
virtual
override(Permit2PaymentMetaTxn, Permit2PaymentIntent)
returns (string memory)
{
return super._witnessTypeSuffix();
}
}
Loading
Loading