Skip to content
47 changes: 47 additions & 0 deletions packages/evm/contracts/Settler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import '@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol';
import '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol';

import './Intents.sol';
import './interfaces/IController.sol';
Expand All @@ -42,6 +43,7 @@ contract Settler is ISettler, Initializable, OwnableUpgradeable, ReentrancyGuard
using IntentsHelpers for Intent;
using IntentsHelpers for Proposal;
using IntentsHelpers for Validation;
using SafeguardsHelpers for SafeguardAuthorization;
using SmartAccountsHandlerHelpers for address;

// Mimic controller reference
Expand All @@ -62,6 +64,9 @@ contract Settler is ISettler, Initializable, OwnableUpgradeable, ReentrancyGuard
// Safeguard config per user
mapping (address => bytes) internal _userSafeguard;

// Whether a safeguard nonce was already used by a user
mapping (address => mapping (uint256 => bool)) public override isUserSafeguardNonceUsed;

/**
* @dev Modifier to tag settler functions in order to check if the sender is an allowed solver
*/
Expand Down Expand Up @@ -177,6 +182,32 @@ contract Settler is ISettler, Initializable, OwnableUpgradeable, ReentrancyGuard
_setSafeguard(msg.sender, safeguard);
}

/**
* @dev Sets a safeguard on behalf of a user based on a signature authorized by that user. The user can be
* an EOA authorizing it with its own ECDSA signature, or a smart account implementing ERC-1271.
Comment thread
lgalende marked this conversation as resolved.
* @param authorization Safeguard authorization signed by the user
* @param signature EIP-712 signature authorizing the safeguard, verified with ECDSA or ERC-1271. It may be
* empty for smart accounts that track approved messages on-chain.
*/
function setSafeguardWithSignature(SafeguardAuthorization memory authorization, bytes memory signature)
external
override
{
uint256 deadline = authorization.deadline;
if (deadline <= block.timestamp) revert SettlerSafeguardPastDeadline(deadline, block.timestamp);

address user = authorization.user;
uint256 nonce = authorization.nonce;
if (isUserSafeguardNonceUsed[user][nonce]) revert SettlerSafeguardNonceAlreadyUsed(user, nonce);

bytes32 typedDataHash = _hashTypedDataV4(authorization.hash());
if (!_isValidUserSignature(user, typedDataHash, signature)) revert SettlerSafeguardInvalidSignature(user);

// Consuming the nonce makes each signature usable only once, no matter who submits it
isUserSafeguardNonceUsed[user][nonce] = true;
_setSafeguard(user, authorization.safeguard);
}

/**
* @dev Executes a proposal to fulfill an intent
* @param intent Intent to be fulfilled
Expand Down Expand Up @@ -666,6 +697,22 @@ contract Settler is ISettler, Initializable, OwnableUpgradeable, ReentrancyGuard
emit DynamicCallEncoderSet(newDynamicCallEncoder);
}

/**
* @dev Tells whether a signature over a hash was authorized by a user, supporting both EOAs and smart
* accounts implementing ERC-1271, such as Safe. ECDSA is attempted first so that EOAs that delegated
* their code (EIP-7702) are still verified as EOAs instead of being routed to their delegate.
* Note: unlike ECDSA signatures, ERC-1271 signatures are revocable, so validity is evaluated at
* execution time and the result may change over time for the same signature.
* @param user Address that must have authorized the signature
* @param hash Hash that was signed
* @param signature Signature to be verified
*/
function _isValidUserSignature(address user, bytes32 hash, bytes memory signature) internal view returns (bool) {
(address signer, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, signature);
if (err == ECDSA.RecoverError.NoError && signer == user) return true;
return SignatureChecker.isValidERC1271SignatureNow(user, hash, signature);
}

/**
* @dev Sets a safeguard for a user
* @param user Address of the user to set the safeguard for
Expand Down
31 changes: 31 additions & 0 deletions packages/evm/contracts/interfaces/ISettler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ interface ISettler {
*/
error SettlerTooManySafeguards(uint256 lengthRequested);

/**
* @dev The safeguard deadline is in the past
*/
error SettlerSafeguardPastDeadline(uint256 deadline, uint256 timestamp);

/**
* @dev The safeguard signature is not authorized by the user
*/
error SettlerSafeguardInvalidSignature(address user);

/**
* @dev The safeguard nonce was already used by the user
*/
error SettlerSafeguardNonceAlreadyUsed(address user, uint256 nonce);

/**
* @dev The chains of a swap operation do not match the swap type (single or cross chain)
*/
Expand Down Expand Up @@ -241,6 +256,13 @@ interface ISettler {
*/
function getUserSafeguard(address user) external view returns (bytes memory);

/**
* @dev Tells whether a safeguard nonce was already used by a user
* @param user Address of the user being queried
* @param nonce Safeguard nonce being queried
*/
function isUserSafeguardNonceUsed(address user, uint256 nonce) external view returns (bool);

/**
* @dev Tells the hash of an intent
* @param intent Intent to get the hash of
Expand Down Expand Up @@ -290,6 +312,15 @@ interface ISettler {
*/
function setSafeguard(bytes memory safeguard) external;

/**
* @dev Sets a safeguard on behalf of a user based on a signature authorized by that user. The user can be
* an EOA authorizing it with its own ECDSA signature, or a smart account implementing ERC-1271.
* @param authorization Safeguard authorization signed by the user
* @param signature EIP-712 signature authorizing the safeguard, verified with ECDSA or ERC-1271. It may be
* empty for smart accounts that track approved messages on-chain.
*/
function setSafeguardWithSignature(SafeguardAuthorization memory authorization, bytes memory signature) external;

/**
* @dev Executes a proposal to fulfill an intent
* @param intent Intent to be fulfilled
Expand Down
32 changes: 32 additions & 0 deletions packages/evm/contracts/safeguards/Safeguards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,35 @@ struct Safeguard {
uint8 mode;
bytes config;
}

/**
* @dev EIP-712 typed data struct representing a user's authorization to set its safeguard
* @param user User the safeguard belongs to
* @param safeguard Encoded safeguard config to be set for the user
* @param nonce Unique value chosen by the user to prevent replay attacks
* @param deadline Timestamp by which the safeguard must be set
*/
struct SafeguardAuthorization {
address user;
bytes safeguard;
uint256 nonce;
uint256 deadline;
}

library SafeguardsHelpers {
bytes32 internal constant SAFEGUARD_AUTHORIZATION_TYPE_HASH =
keccak256('SafeguardAuthorization(address user,bytes safeguard,uint256 nonce,uint256 deadline)');

function hash(SafeguardAuthorization memory authorization) internal pure returns (bytes32) {
return
keccak256(
abi.encode(
SAFEGUARD_AUTHORIZATION_TYPE_HASH,
authorization.user,
keccak256(authorization.safeguard),
authorization.nonce,
authorization.deadline
)
);
}
}
2 changes: 2 additions & 0 deletions packages/evm/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const config: HardhatUserConfig = {
default: {
version: '0.8.28',
settings: {
// The IR pipeline is required to keep Settler under the EIP-170 24576-byte limit
viaIR: true,
optimizer: {
enabled: true,
runs: 1000,
Expand Down
Loading
Loading