Skip to content

Commit

Permalink
Merge pull request #821 from lidofinance/sepolia-deposit-adapter-impl
Browse files Browse the repository at this point in the history
Sepolia deposit adapter implementation
  • Loading branch information
TheDZhon committed Feb 21, 2024
2 parents 6886688 + 2480651 commit c72ce19
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/storage-layout-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
mode: check
src-folder: ./contracts
ignore-folders: '{test_helpers,template,mocks}'
ignore-contracts: 'WithdrawalsManagerProxy|WithdrawalsManagerStub|ERC1967Proxy'
ignore-contracts: 'WithdrawalsManagerProxy|WithdrawalsManagerStub|ERC1967Proxy|SepoliaDepositAdapter'
91 changes: 84 additions & 7 deletions contracts/0.8.9/SepoliaDepositAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,101 @@
/* See contracts/COMPILERS.md */
pragma solidity 0.8.9;

import "@openzeppelin/contracts-v4.4/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-v4.4/access/Ownable.sol";

contract SepoliaDepositAdapter {

uint public constant TEST_VALUE = 16;
address public immutable depositContract;
interface IDepositContract {
event DepositEvent(
bytes pubkey,
bytes withdrawal_credentials,
bytes amount,
bytes signature,
bytes index
);

function deposit(
bytes calldata pubkey,
bytes calldata withdrawal_credentials,
bytes calldata signature,
bytes32 deposit_data_root
) external payable {
}
) external payable;

function get_deposit_root() external view returns (bytes32);

function get_deposit_count() external view returns (bytes memory);
}

// Sepolia deposit contract variant of the source code https://github.com/protolambda/testnet-dep-contract/blob/master/deposit_contract.sol
interface ISepoliaDepositContract is IDepositContract, IERC20 { }

// Sepolia testnet deposit contract have a bit different logic than the mainnet deposit contract.
// The differences are:
// 1. Sepolia contract require specific Bepolia token to be used for depositing. It burns this token after depositing.
// 2. It returns the ETH to the sender after depositing.
// This adapter is used to make the mainnet deposit contract compatible with the testnet deposit contract.
// For further information see Sepolia deposit contract variant source code link above.
contract SepoliaDepositAdapter is IDepositContract, Ownable {

event EthReceived(address sender, uint256 amount);

event EthRecovered(uint256 amount);

event BepoliaRecovered(uint256 amount);

error EthRecoverFailed();

error BepoliaRecoverFailed();

error DepositFailed();

ISepoliaDepositContract public immutable originalContract;

constructor(address _deposit_contract) {
depositContract = _deposit_contract;
originalContract = ISepoliaDepositContract(_deposit_contract);
}

function get_deposit_root() override external view returns (bytes32) {
return originalContract.get_deposit_root();
}

function get_deposit_count() override external view returns (bytes memory) {
return originalContract.get_deposit_count();
}

receive() external payable {
emit EthReceived(msg.sender, msg.value);
}

function recoverEth() external onlyOwner {
uint256 balance = address(this).balance;
// solhint-disable-next-line avoid-low-level-calls
(bool success,) = owner().call{value: balance}("");
if (!success) {
revert EthRecoverFailed();
}
emit EthRecovered(balance);
}

function recoverBepolia() external onlyOwner {
uint256 bepoliaOwnTokens = originalContract.balanceOf(address(this));
bool success = originalContract.transfer(owner(), bepoliaOwnTokens);
if (!success) {
revert BepoliaRecoverFailed();
}
emit BepoliaRecovered(bepoliaOwnTokens);
}

function deposit(
bytes calldata pubkey,
bytes calldata withdrawal_credentials,
bytes calldata signature,
bytes32 deposit_data_root
) override external payable {
originalContract.deposit{value: msg.value}(pubkey, withdrawal_credentials, signature, deposit_data_root);
// solhint-disable-next-line avoid-low-level-calls
(bool success,) = owner().call{value: msg.value}("");
if (!success) {
revert DepositFailed();
}
}
}
3 changes: 3 additions & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ const getNetConfig = (networkName, ethAccountName) => {
if (networkName === 'hardhat' && process.env.HARDHAT_FORKING_URL) {
netConfig.forking = { url: process.env.HARDHAT_FORKING_URL }
}
if (networkName === 'hardhat' && process.env.HARDHAT_CHAIN_ID) {
netConfig.chainId = +process.env.HARDHAT_CHAIN_ID
}
return netConfig ? { [networkName]: netConfig } : {}
}

Expand Down
158 changes: 158 additions & 0 deletions test/0.8.9/sepolia-deposit-adapter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const { contract, artifacts, ethers } = require('hardhat')
const { assert } = require('../helpers/assert')
const { ETH } = require('../helpers/utils')

const { EvmSnapshot } = require('../helpers/blockchain')

const SepoliaDepositAdapter = artifacts.require('SepoliaDepositAdapter')
const SepoliaDepositContract = artifacts.require('ISepoliaDepositContract')

// To run Sepolia Deposit Adapter tests:
// HARDHAT_FORKING_URL=<rpc url> HARDHAT_CHAIN_ID=11155111 npx hardhat test --grep "SepoliaDepositAdapter"
contract('SepoliaDepositAdapter', ([deployer]) => {
let depositAdapter
let snapshot
let bepoliaToken
const sepoliaDepositContractAddress = '0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D'
const EOAddress = '0x6885E36BFcb68CB383DfE90023a462C03BCB2AE5'
const bepoliaTokenHolder = EOAddress
// const log = console.log
const log = () => {}

before('deploy lido with dao', async function () {
const { chainId } = await ethers.provider.getNetwork()
if (chainId !== 11155111) {
return this.skip()
}

depositAdapter = await SepoliaDepositAdapter.new(sepoliaDepositContractAddress)
log('depositAdapter address', depositAdapter.address)

bepoliaToken = await ethers.getContractAt('ISepoliaDepositContract', sepoliaDepositContractAddress)

const code = await ethers.provider.getCode(depositAdapter.address)
assert.notEqual(code, '0x')

snapshot = new EvmSnapshot(ethers.provider)
await snapshot.make()
})

afterEach(async () => {
await snapshot.rollback()
})

describe('SepoliaDepositAdapter Logic', () => {
it(`recover Bepolia tokens`, async () => {
const adapterAddr = depositAdapter.address
const BEPOLIA_TO_TRANSFER = 2
const bepoliaHolderInitialBalance = await bepoliaToken.balanceOf(bepoliaTokenHolder)
const impersonatedSigner = await ethers.getImpersonatedSigner(bepoliaTokenHolder)

log('bepoliaHolderInitialBalance', bepoliaHolderInitialBalance)
await bepoliaToken.connect(impersonatedSigner).transfer(adapterAddr, BEPOLIA_TO_TRANSFER)

assert.equals(await bepoliaToken.balanceOf(adapterAddr), BEPOLIA_TO_TRANSFER)

const bepoliaHolderEndBalance = await bepoliaToken.balanceOf(bepoliaTokenHolder)
assert.equals(bepoliaHolderEndBalance, bepoliaHolderInitialBalance - BEPOLIA_TO_TRANSFER)
log('bepoliaHolderEndBalance', bepoliaHolderEndBalance)

// Recover Bepolia tokens
const receipt = await depositAdapter.recoverBepolia()
assert.emits(receipt, 'BepoliaRecovered', { amount: BEPOLIA_TO_TRANSFER })

const bepoliaTokensOnAdapter = await bepoliaToken.balanceOf(adapterAddr)
assert.equals(bepoliaTokensOnAdapter, 0)

const [owner] = await ethers.getSigners()
const bepoliaTokenHolderEnd = await bepoliaToken.balanceOf(owner.address)
assert.equals(bepoliaTokenHolderEnd, BEPOLIA_TO_TRANSFER)
})

it(`call deposit on Adapter`, async () => {
const key = '0x90823dc2e5ab8a52a0b32883ea8451cbe4c921a42ce439f4fb306a90e9f267e463241da7274b6d44c2e4b95ddbcb0ad3'
const withdrawalCredentials = '0x005bfe00d82068a0c2a6687afaf969dad5a9c663cb492815a65d203885aaf993'
const sig =
'0x802899068eb4b37c95d46869947cac42b9c65b90fcb3fde3854c93ad5737800c01e9c82e174c8ed5cc18210bd60a94ea0082a850817b1dddd4096059b6846417b05094c59d3dd7f4028ed9dff395755f9905a88015b0ed200a7ec1ed60c24922'
const dataRoot = '0x8b09ed1d0fb3b8e3bb8398c6b77ee3d8e4f67c23cb70555167310ef02b06e5f5'

const adapterAddr = depositAdapter.address

const balance0ETH = await ethers.provider.getBalance(adapterAddr)
assert.equals(balance0ETH, 0)

const impersonatedSigner = await ethers.getImpersonatedSigner(bepoliaTokenHolder)
// Transfer 1 Bepolia token to depositCaller
await bepoliaToken.connect(impersonatedSigner).transfer(adapterAddr, 1)

const [owner] = await ethers.getSigners()
log('owner', owner.address)

const bepoliaTokenHolderBalance = await bepoliaToken.balanceOf(bepoliaTokenHolder)
const adapterBepoliaBalance = await bepoliaToken.balanceOf(adapterAddr)
log('bepoliaTokenHolder and adapter balances: ', bepoliaTokenHolderBalance, adapterBepoliaBalance)
// We need to have exactly 1 Bepolia token in the adapter
assert.equals(adapterBepoliaBalance, 1)

const depositRootBefore = await depositAdapter.get_deposit_root()
log('depositRoot', depositRootBefore)
const depositCountBefore = await depositAdapter.get_deposit_count()
log('depositCount', depositCountBefore)

const sepoliaDepositContract = await SepoliaDepositContract.at(sepoliaDepositContractAddress)

const receipt = await depositAdapter.deposit(key, withdrawalCredentials, sig, dataRoot, {
from: owner.address,
value: ETH(32),
})
assert.emits(receipt, 'EthReceived', { sender: sepoliaDepositContractAddress, amount: ETH(32) })
const depositEvents = await sepoliaDepositContract.getPastEvents('DepositEvent')
assert.equals(depositEvents.length, 1)
log('depositEvents', depositEvents, ETH(32))

assert.equals(depositEvents[0].args.pubkey, key)
assert.equals(depositEvents[0].args.withdrawal_credentials, withdrawalCredentials)
assert.equals(depositEvents[0].args.signature, sig)

const depositRootAfter = await depositAdapter.get_deposit_root()
log('depositRoot After', depositRootAfter)
const depositCountAfter = await depositAdapter.get_deposit_count()
log('depositCount After', depositCountAfter)
assert.notEqual(depositRootBefore, depositRootAfter)
assert.equals(BigInt(depositCountBefore) + BigInt('0x0100000000000000'), BigInt(depositCountAfter))

const ethAfterDeposit = await ethers.provider.getBalance(adapterAddr)
log('ethAfterDeposit', ethAfterDeposit.toString())
assert.equals(ethAfterDeposit, 0)

const adapterBepoliaBalanceAfter = await bepoliaToken.balanceOf(adapterAddr)
assert.equals(adapterBepoliaBalanceAfter, 0)
})

it(`recover ETH`, async () => {
const ETH_TO_TRANSFER = ETH(10)
const adapterAddr = depositAdapter.address

const balance0ETH = await ethers.provider.getBalance(adapterAddr)
assert.equals(balance0ETH, 0)

const [owner] = await ethers.getSigners()
log('owner', owner.address)
await owner.sendTransaction({
to: adapterAddr,
value: ETH_TO_TRANSFER,
})

const ethAfterDeposit = await ethers.provider.getBalance(adapterAddr)
log('ethAfterDeposit', ethAfterDeposit.toString())
assert.equals(ethAfterDeposit, ETH_TO_TRANSFER)

const receipt = await depositAdapter.recoverEth()
assert.emits(receipt, 'EthRecovered', { amount: ETH_TO_TRANSFER })

const balanceEthAfterRecover = await ethers.provider.getBalance(adapterAddr)
log('balanceEthAfterRecover', balanceEthAfterRecover.toString())
assert.equals(balanceEthAfterRecover, 0)
})
})
})

0 comments on commit c72ce19

Please sign in to comment.