Skip to content

Instantly share code, notes, and snippets.

@ecmendenhall
Last active December 23, 2022 08:34
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ecmendenhall/9408082d8f3cfe50076642d8745fc6d3 to your computer and use it in GitHub Desktop.
Save ecmendenhall/9408082d8f3cfe50076642d8745fc6d3 to your computer and use it in GitHub Desktop.
Seaport Token Transfers

Seaport token transfer examples

contract TransferFrom{
function _performERC20Transfer(
address token,
address from,
address to,
uint256 amount
) internal {
bool success = IERC20(token).transferFrom(
from,
to,
amount
);
require(success, "Transfer failed");
}
}
contract OZSafeERC20 {
function _performERC20Transfer(
address token,
address from,
address to,
uint256 amount
) internal {
SafeERC20.safeTransferFrom(
IERC20(token),
from,
to,
amount
);
}
}
contract OpenZeppelin {
function _performERC20Transfer(
address token,
address from,
address to,
uint256 amount
) internal {
// Encodes calldata for transferFrom(address,address,uint256).
// This is 100 bytes long: the 4 byte function selector, plus 32
// bytes each for the address, address, uint256 arguments.
//
// It looks like this:
//
// 0x23b872dd <32 bytes from address> <32 bytes to address> <32 bytes amount>
bytes memory data = abi.encodeWithSelector(
IERC20(token).transferFrom.selector,
from,
to,
value
);
_callOptionalReturn(IERC20(token), data);
}
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data
// size checking mechanism, since we're implementing it ourselves. We use
// _functionCallWithValue to perform this call, which verifies that the target
// address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = _functionCallWithValue(
address(token),
data,
0,
"SafeERC20: low-level call failed"
);
// Some ERC20s don't return a boolean. If it hasn't reverted, assume success.
if (returndata.length > 0) {
// If we do have a return value, decode it.
require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
}
function _functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
require(isContract(target), "Address: call to non-contract");
// Make a low-level call to the target address using the provided calldata
(bool success, bytes memory returndata) = target.call{value: value}(data);
// Check whether the call succeeded or failed. The returndata value might be a
// boolean if the call succeeded, or an error message if it reverted.
return _verifyCallResult(success, returndata, errorMessage);
}
function _verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
}
contract Solmate {
function _performERC20Transfer(
address token,
address from,
address to,
uint256 amount
) internal {
// We're gonna do the same thing as in the previous example using
// OpenZeppelin SafeTransfer, but use assembly to build up the calldata,
// make the call, and check the return value for an error.
// Set a success flag outside of inline assembly.
// Note that this is false by default!
bool success;
assembly {
// Get the free memory pointer. This is a pointer to the current location
// in memory where we can safely write new data.
let freeMemoryPointer := mload(0x40)
// Write the abi-encoded calldata into memory, beginning with the function selector.
// This is the equivalent of using `abi.encodeWithSelector` in the previous example.
// Store the function selector at the start of free memory.
mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(freeMemoryPointer, 4), from) // Append the "from" argument, bytes 4-36.
mstore(add(freeMemoryPointer, 36), to) // Append the "to" argument, bytes 36-68.
mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument, bytes 68-100.
success := and(
// Set success to whether the call reverted, if not we check it either
// returned exactly 1 (can't just be non-zero data), or had no return data.
// Use assembly to check if the call reverted. This is the equivalent of
// the checks in `_callOptionalReturn` and `_verifyCallResult` in the
// previous example.
or(
and(
eq(mload(0), 1), // returndata value is exactly 1 (i.e. true)
gt(returndatasize(), 31) // returndata is at least 32 bytes
),
iszero(returndatasize()) // Call had no returndata
),
// We use 100 because the length of our calldata totals up like so: 4 + 32 * 3.
// We use 0 and 32 to copy up to 32 bytes of return data into the scratch space.
// Counterintuitively, this call must be positioned second to the or() call in the
// surrounding and() call or else returndatasize() will be zero during the computation.
call(
gas(), // Pass along remaining gas
token, // Call the token address
0, // Send no ETH value
freeMemoryPointer, // Calldata starts at the free memory pointer
100, // Calldata is 100 bytes long
0, // Store the return value at offset zero in memory
32 // Return value is 32 bytes long
)
)
}
// This approach is gas-optimized, but we dont' get as much information about
// why a particular call failed. Instead, we have to return a pretty generic
// error message:
require(success, "TRANSFER_FROM_FAILED");
}
}
contract Seaport {
function _performERC20Transfer(
address token,
address from,
address to,
uint256 amount
) internal {
// Utilize assembly to perform an optimized ERC20 token transfer.
assembly {
// Build our `transferFrom` calldata. This is basically the same
// the previous Solmate example, but now all the values have names
// and the offsets are precalculated.
// Write calldata to the free memory pointer, but restore it later.
let memPointer := mload(FreeMemoryPointerSlot)
// Write calldata into memory, starting with function selector.
mstore(ERC20_transferFrom_sig_ptr, ERC20_transferFrom_signature) // Function signature
mstore(ERC20_transferFrom_from_ptr, from) // "from" argument
mstore(ERC20_transferFrom_to_ptr, to) // "to" argument
mstore(ERC20_transferFrom_amount_ptr, amount) // "amount" argument
// Make call & copy up to 32 bytes of return data to scratch space.
let callStatus := call(
gas(), // Forward remaining gas
token, // Call token address
0, // Send zero ETH
ERC20_transferFrom_sig_ptr, // Calldata starts at transferFrom pointer
ERC20_transferFrom_length, // Calldata is 100 bytes long
0, // Store return value at memory offset 0
OneWord // Returndata is one word (32 bytes) long
)
// Determine whether transfer was successful using status & result.
// This should look familiar from Solmate: it's the same check!
let success := and(
// Set success to whether the call reverted, if not check it
// either returned exactly 1 (can't just be non-zero data), or
// had no return data.
or(
and(eq(mload(0), 1), gt(returndatasize(), 31)),
iszero(returndatasize())
),
callStatus
)
// Unlike Solmate, we're going to try to detect what went wrong
// and return a more meaningful error. There are a few different
// cases here:
//
// - Bubble up the original error message if we have enough gas
// - Return an error indicating a bad return value
// - Return an error indicating that the contract has no code
// - Return a generic error message
// If the transfer failed or it returned nothing:
// Group these because they should be uncommon.
// Equivalent to `or(iszero(success), iszero(returndatasize()))`
// but after it's inverted for JUMPI this expression is cheaper.
if iszero(and(success, iszero(iszero(returndatasize())))) {
// If the token has no code or the transfer failed:
// Equivalent to `or(iszero(success), iszero(extcodesize(token)))`
// but after it's inverted for JUMPI this expression is cheaper.
if iszero(and(iszero(iszero(extcodesize(token))), success)) {
// If the transfer failed:
if iszero(success) {
// If it was due to a revert:
if iszero(callStatus) {
// If it returned a message, bubble it up as long as
// sufficient gas remains to do so:
if returndatasize() {
// Ensure that sufficient gas is available to
// copy returndata while expanding memory where
// necessary. Start by computing the word size
// of returndata and allocated memory.
let returnDataWords := div(
returndatasize(),
OneWord
)
// Note: use the free memory pointer in place of
// msize() to work around a Yul warning that
// prevents accessing msize directly when the IR
// pipeline is activated.
let msizeWords := div(memPointer, OneWord)
// Next, compute the cost of the returndatacopy.
let cost := mul(CostPerWord, returnDataWords)
// Then, compute cost of new memory allocation.
if gt(returnDataWords, msizeWords) {
cost := add(
cost,
add(
mul(
sub(
returnDataWords,
msizeWords
),
CostPerWord
),
div(
sub(
mul(
returnDataWords,
returnDataWords
),
mul(msizeWords, msizeWords)
),
MemoryExpansionCoefficient
)
)
)
}
// Finally, add a small constant and compare to
// gas remaining; bubble up the revert data if
// enough gas is still available.
if lt(add(cost, ExtraGasBuffer), gas()) {
// Copy returndata to memory; overwrite
// existing memory.
returndatacopy(0, 0, returndatasize())
// Revert, specifying memory region with
// copied returndata.
revert(0, returndatasize())
}
}
// Otherwise revert with a generic error message.
mstore(
TokenTransferGenericFailure_error_sig_ptr,
TokenTransferGenericFailure_error_signature
)
mstore(
TokenTransferGenericFailure_error_token_ptr,
token
)
mstore(
TokenTransferGenericFailure_error_from_ptr,
from
)
mstore(TokenTransferGenericFailure_error_to_ptr, to)
mstore(TokenTransferGenericFailure_error_id_ptr, 0)
mstore(
TokenTransferGenericFailure_error_amount_ptr,
amount
)
revert(
TokenTransferGenericFailure_error_sig_ptr,
TokenTransferGenericFailure_error_length
)
}
// Otherwise revert with a message about the token
// returning false.
mstore(
BadReturnValueFromERC20OnTransfer_error_sig_ptr,
BadReturnValueFromERC20OnTransfer_error_signature
)
mstore(
BadReturnValueFromERC20OnTransfer_error_token_ptr,
token
)
mstore(
BadReturnValueFromERC20OnTransfer_error_from_ptr,
from
)
mstore(
BadReturnValueFromERC20OnTransfer_error_to_ptr,
to
)
mstore(
BadReturnValueFromERC20OnTransfer_error_amount_ptr,
amount
)
revert(
BadReturnValueFromERC20OnTransfer_error_sig_ptr,
BadReturnValueFromERC20OnTransfer_error_length
)
}
// Otherwise revert with error about token not having code:
mstore(NoContract_error_sig_ptr, NoContract_error_signature)
mstore(NoContract_error_token_ptr, token)
revert(NoContract_error_sig_ptr, NoContract_error_length)
}
// Otherwise the token just returned nothing but otherwise
// succeeded; no need to optimize for this as it's not
// technically ERC20 compliant.
}
// Restore the original free memory pointer.
mstore(FreeMemoryPointerSlot, memPointer)
// Restore the zero slot to zero.
mstore(ZeroSlot, 0)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment