-
-
Save ecmendenhall/9408082d8f3cfe50076642d8745fc6d3 to your computer and use it in GitHub Desktop.
Seaport Token Transfers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
contract OZSafeERC20 { | |
function _performERC20Transfer( | |
address token, | |
address from, | |
address to, | |
uint256 amount | |
) internal { | |
SafeERC20.safeTransferFrom( | |
IERC20(token), | |
from, | |
to, | |
amount | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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