Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import { BaseAdaptor, ERC20, SafeTransferLib } from "src/modules/adaptors/BaseAdaptor.sol";

/**
* @title ERC20 Adaptor
* @notice Allows Cellars to interact with ERC20 positions.
* @author crispymangoes
*/
contract ERC20Adaptor is BaseAdaptor {
using SafeTransferLib for ERC20;

//==================== Adaptor Data Specification ====================
// adaptorData = abi.encode(ERC20 token)
// Where:
// `token` is the underling ERC20 token this adaptor is working with
//================= Configuration Data Specification =================
// isLiquid bool
// Indicates whether the position is liquid or not.
//====================================================================

//============================================ Global Functions ===========================================
/**
* @dev Identifier unique to this adaptor for a shared registry.
* Normally the identifier would just be the address of this contract, but this
* Identifier is needed during Cellar Delegate Call Operations, so getting the address
* of the adaptor is more difficult.
*/
function identifier() public pure override returns (bytes32) {
return keccak256(abi.encode("ERC20 Adaptor Withdrawal Recovery Candidate V 1.0"));
}

//============================================ Implement Base Functions ===========================================
/**
* @notice Cellar already has possession of users ERC20 assets by the time this function is called,
* so there is nothing to do.
*/
function deposit(uint256, bytes memory, bytes memory) public override {}

/**
* @notice Cellar just needs to transfer ERC20 token to `receiver`.
* @dev Important to verify that external receivers are allowed if receiver is not Cellar address.
* @param assets amount of `token` to send to receiver
* @param receiver address to send assets to
* @param adaptorData data needed to withdraw from this position
* @param configurationData data needed to determine if this position is liquid or not
*/
function withdraw(
uint256 assets,
address receiver,
bytes memory adaptorData,
bytes memory configurationData
) public override {
_externalReceiverCheck(receiver);

bool isLiquid = abi.decode(configurationData, (bool));
if (!isLiquid) revert BaseAdaptor__UserWithdrawsNotAllowed();

ERC20 token = abi.decode(adaptorData, (ERC20));
token.safeTransfer(receiver, assets);
}

/**
* @notice Identical to `balanceOf`, if an asset is used with a non ERC20 standard locking logic,
* then a NEW adaptor contract is needed.
*/
function withdrawableFrom(
bytes memory adaptorData,
bytes memory configurationData
) public view override returns (uint256) {
bool isLiquid = abi.decode(configurationData, (bool));
if (isLiquid) {
ERC20 token = abi.decode(adaptorData, (ERC20));
return token.balanceOf(msg.sender);
} else return 0;
}

/**
* @notice Returns the balance of `token`.
*/
function balanceOf(bytes memory adaptorData) public view override returns (uint256) {
ERC20 token = abi.decode(adaptorData, (ERC20));
return token.balanceOf(msg.sender);
}

/**
* @notice Returns `token`
*/
function assetOf(bytes memory adaptorData) public pure override returns (ERC20) {
ERC20 token = abi.decode(adaptorData, (ERC20));
return token;
}

/**
* @notice This adaptor returns collateral, and not debt.
*/
function isDebt() public pure override returns (bool) {
return false;
}

//============================================ Strategist Functions ===========================================
}
44 changes: 44 additions & 0 deletions deployments/optimism/ryeth-withdrawal-recovery-adaptor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"chainId": 10,
"network": "optimism",
"contract": "ERC20WithdrawalRecoveryAdaptor",
"address": "0x214ff95df1Ec8238977dACAeD9768441ce7d05dc",
"deploymentTransaction": "0x65bb90168ba5a3459d09c631105fb7cf8e53ac3820069b77f6927278dc02477d",
"deploymentBlock": 157785132,
"deployer": "0x096CA3674329bB66dD7CC14D1511dfB7728b9193",
"compiler": {
"version": "0.8.21",
"optimizer": true,
"optimizerRuns": 200,
"evmVersion": "shanghai"
},
"identifier": "0x711b45880aefcfc7e41d3d822547acaa8105eb22c3b1b224c84f11f2cb205410",
"runtimeBytecodeKeccak256": "0x0d037adbce330a32869a4ba8a29f93c56aa3767094a9185405061b2c6a38fb12",
"metadataFreeRuntimeBytecodeKeccak256": "0x5509c54323791d5da52562bf42d23f36858c1bd57454abe30ad056f5fc3ff0cb",
"sourceCommit": "07efdfa9ae15beb66d248722cbbd596926b5d55f",
"sourceChange": "The deployed source copied ERC20Adaptor and changed only its identifier string to ERC20 Adaptor Withdrawal Recovery Candidate V 1.0.",
"repositoryRepresentation": "The repository contract inherits ERC20Adaptor and overrides only identifier(). Its metadata-free runtime is byte-for-byte identical to the deployed runtime. Solidity metadata differs because the repository uses a distinct source path and contract name.",
"identifierNote": "The identifier string includes 'Candidate' and is fixed by the deployed bytecode. Do not rename it in source; a different string is a different identifier.",
"deployedSource": {
"path": "deployments/optimism/ryeth-withdrawal-recovery-adaptor.deployed-source.sol",
"sha256": "689ed6bca87c519a35a83bfa900badd50fc249d42a46d1edeec16030bd7901f6",
"compileAs": {
"path": "src/modules/adaptors/ERC20Adaptor.sol",
"contractName": "ERC20Adaptor",
"atCommit": "07efdfa9ae15beb66d248722cbbd596926b5d55f"
},
"reproduces": "Full runtime bytecode including metadata (keccak256 0x0d037adbce330a32869a4ba8a29f93c56aa3767094a9185405061b2c6a38fb12). Use this source and path for block explorer verification."
},
"intendedUse": {
"vault": "0xC47bB288178Ea40bF520a91826a3DEE9e0DbFA4C",
"registry": "0xb7f57c1a22e9cbac58016cd01749433f390091bb",
"token": "0x9Bcef72be871e61ED4fBbc7630889beE758eb81D",
"tokenSymbol": "rETH",
"replacesPositionId": 9,
"replacesPositionAdaptor": "0x2fD86717d9Bba566Fe637Ac6D3C69369CE998555",
"recoveryPositionId": 9999,
"invariant": "Position 9 (rETH via ERC20Adaptor) must be removed from the vault before or in the same operation as adding position 9999. Both adaptors report the vault's full rETH balance, so holding both counts rETH twice in totalAssets.",
"liveStateCheckedAtBlock": 157789515,
"liveStateAtCheck": "Vault credit positions [7, 8, 9, 16, 17, 18, 43]; position 9 is the only ERC20Adaptor rETH position; adaptor not yet trusted in the registry; position 9999 not yet registered."
}
}
2 changes: 1 addition & 1 deletion src/modules/adaptors/ERC20Adaptor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ contract ERC20Adaptor is BaseAdaptor {
* Identifier is needed during Cellar Delegate Call Operations, so getting the address
* of the adaptor is more difficult.
*/
function identifier() public pure override returns (bytes32) {
function identifier() public pure virtual override returns (bytes32) {
return keccak256(abi.encode("ERC20 Adaptor V 1.0"));
}

Expand Down
24 changes: 24 additions & 0 deletions src/modules/adaptors/ERC20WithdrawalRecoveryAdaptor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import {ERC20Adaptor} from "src/modules/adaptors/ERC20Adaptor.sol";

/**
* @title ERC20 Withdrawal Recovery Adaptor
* @notice Isolates recovery ERC20 positions from positions that use the standard ERC20 adaptor identifier.
* @dev WARNING: `balanceOf` reports the Cellar's full balance of the token, exactly like `ERC20Adaptor`.
* The distinct identifier lets the Registry trust a second position for a token that already has an
* `ERC20Adaptor` position, and `Cellar.addPosition` does not reject two positions for the same asset.
* A Cellar must never hold this position alongside an `ERC20Adaptor` position for the same token, or
* that token is counted twice in `totalAssets`. Replace the existing position; do not add beside it.
*/
contract ERC20WithdrawalRecoveryAdaptor is ERC20Adaptor {
/**
* @dev Identifier unique to withdrawal recovery positions in a shared registry.
* The string includes "Candidate" because it matches the deployed contract; changing it would change the
* identifier and no longer describe that deployment.
*/
function identifier() public pure override returns (bytes32) {
return keccak256(abi.encode("ERC20 Adaptor Withdrawal Recovery Candidate V 1.0"));
}
}
42 changes: 42 additions & 0 deletions test/testAdaptors/ERC20WithdrawalRecoveryAdaptor.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.21;

import {Test} from "@forge-std/Test.sol";
import {ERC20Adaptor} from "src/modules/adaptors/ERC20Adaptor.sol";
import {ERC20WithdrawalRecoveryAdaptor} from "src/modules/adaptors/ERC20WithdrawalRecoveryAdaptor.sol";

contract ERC20WithdrawalRecoveryAdaptorTest is Test {
ERC20WithdrawalRecoveryAdaptor private adaptor;

function setUp() external {
adaptor = new ERC20WithdrawalRecoveryAdaptor();
}

function testIdentifierIsIsolatedFromTheStandardERC20Adaptor() external {
ERC20Adaptor standardAdaptor = new ERC20Adaptor();

assertEq(standardAdaptor.identifier(), keccak256(abi.encode("ERC20 Adaptor V 1.0")));
assertEq(adaptor.identifier(), keccak256(abi.encode("ERC20 Adaptor Withdrawal Recovery Candidate V 1.0")));
}

function testRecoveryPositionIsCollateral() external {
assertFalse(adaptor.isDebt());
}

/// @dev Intentional pin to the deployed Optimism runtime (metadata stripped). This fails on any change to the
/// adaptor's logic or to compiler settings (version, optimizer runs, EVM version). Do not update the hash to make
/// it pass; a mismatch means this source no longer describes the deployed contract.
function testRuntimeMatchesDeployedSemanticBytecode() external {
bytes memory runtimeCode = type(ERC20WithdrawalRecoveryAdaptor).runtimeCode;
uint256 metadataLength = (uint256(uint8(runtimeCode[runtimeCode.length - 2])) << 8)
| uint256(uint8(runtimeCode[runtimeCode.length - 1]));
uint256 semanticLength = runtimeCode.length - metadataLength - 2;
bytes32 semanticHash;

assembly {
semanticHash := keccak256(add(runtimeCode, 0x20), semanticLength)
}

assertEq(semanticHash, 0x5509c54323791d5da52562bf42d23f36858c1bd57454abe30ad056f5fc3ff0cb);
}
}
Loading