Skip to content

Commit

Permalink
feat: add transfer test that fails
Browse files Browse the repository at this point in the history
  • Loading branch information
idea404 committed Nov 10, 2023
1 parent e8bda63 commit 0845045
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 13 deletions.
239 changes: 239 additions & 0 deletions contracts/PensionAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 1,239 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IAccount.sol";
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractHelper.sol";
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/EfficientCall.sol";
import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT, INonceHolder} from "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";


contract PensionAccount is IAccount {
// to get transaction hash
using TransactionHelper for *;

// Owner of the account
address public owner;

// Addresses for DEX and tokens
address public dex;
address public DOGE;
address public PEPE;
address public SHIB;
address public BTC;

// Expiry timestamp
uint256 public expiryTimestamp;

// Event for swap action
event Swap(address indexed token, uint256 amountToSwap);

constructor(address _owner, address _dex, address _doge, address _pepe, address _shib, address _btc) {
owner = _owner;
dex = _dex;
DOGE = _doge;
PEPE = _pepe;
SHIB = _shib;
BTC = _btc;

// Set the expiry timestamp to 3 minutes from now
expiryTimestamp = block.timestamp 3 minutes;
}

// Modifier to check if the time lock has expired
modifier afterExpiry() {
require(block.timestamp >= expiryTimestamp, "Action locked until expiry time");
_;
}

receive() external payable {
uint256 amountToSwap = msg.value / 4;
emit Swap(DOGE, amountToSwap);
emit Swap(PEPE, amountToSwap);
emit Swap(SHIB, amountToSwap);
emit Swap(BTC, amountToSwap);
}


// Override the executeTransaction function to include the time lock
function executeTransaction(
bytes32, // _txHash
bytes32, // _suggestedSignedHash
Transaction calldata _transaction
) external payable ignoreNonBootloader ignoreInDelegateCall afterExpiry {
_execute(_transaction);
}

// Override the payForTransaction function to include the time lock
function payForTransaction(
bytes32, // _txHash
bytes32, // _suggestedSignedHash
Transaction calldata _transaction
) external payable ignoreNonBootloader ignoreInDelegateCall afterExpiry {
bool success = _transaction.payToTheBootloader();
require(success, "Failed to pay the fee to the operator");
}

// Override the prepareForPaymaster function to include the time lock
function prepareForPaymaster(
bytes32, // _txHash
bytes32, // _suggestedSignedHash
Transaction calldata _transaction
) external payable ignoreNonBootloader ignoreInDelegateCall afterExpiry {
_transaction.processPaymasterInput();
}

// =================================================================================================================
// START DEFAULT ACCOUNT CODE
// =================================================================================================================

/**
* @dev Simulate the behavior of the EOA if the caller is not the bootloader.
* Essentially, for all non-bootloader callers halt the execution with empty return data.
* If all functions will use this modifier AND the contract will implement an empty payable fallback()
* then the contract will be indistinguishable from the EOA when called.
*/
modifier ignoreNonBootloader() {
if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) {
// If function was called outside of the bootloader, behave like an EOA.
assembly {
return(0, 0)
}
}
// Continue execution if called from the bootloader.
_;
}

/**
* @dev Simulate the behavior of the EOA if it is called via `delegatecall`.
* Thus, the default account on a delegate call behaves the same as EOA on Ethereum.
* If all functions will use this modifier AND the contract will implement an empty payable fallback()
* then the contract will be indistinguishable from the EOA when called.
*/
modifier ignoreInDelegateCall() {
address codeAddress = SystemContractHelper.getCodeAddress();
if (codeAddress != address(this)) {
// If the function was delegate called, behave like an EOA.
assembly {
return(0, 0)
}
}

// Continue execution if not delegate called.
_;
}

/// @notice Validates the transaction & increments nonce.
/// @dev The transaction is considered accepted by the account if
/// the call to this function by the bootloader does not revert
/// and the nonce has been set as used.
/// @param _suggestedSignedHash The suggested hash of the transaction to be signed by the user.
/// This is the hash that is signed by the EOA by default.
/// @param _transaction The transaction structure itself.
/// @dev Besides the params above, it also accepts unused first paramter "_txHash", which
/// is the unique (canonical) hash of the transaction.
function validateTransaction(
bytes32, // _txHash
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override ignoreNonBootloader ignoreInDelegateCall returns (bytes4 magic) {
magic = _validateTransaction(_suggestedSignedHash, _transaction);
}

/// @notice Inner method for validating transaction and increasing the nonce
/// @param _suggestedSignedHash The hash of the transaction signed by the EOA
/// @param _transaction The transaction.
function _validateTransaction(
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) internal returns (bytes4 magic) {
// Note, that nonce holder can only be called with "isSystem" flag.
SystemContractsCaller.systemCallWithPropagatedRevert(
uint32(gasleft()),
address(NONCE_HOLDER_SYSTEM_CONTRACT),
0,
abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce))
);

// Even though for the transaction types present in the system right now,
// we always provide the suggested signed hash, this should not be
// always expected. In case the bootloader has no clue what the default hash
// is, the bytes32(0) will be supplied.
bytes32 txHash = _suggestedSignedHash != bytes32(0) ? _suggestedSignedHash : _transaction.encodeHash();

// The fact there is are enough balance for the account
// should be checked explicitly to prevent user paying for fee for a
// transaction that wouldn't be included on Ethereum.
uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
require(totalRequiredBalance <= address(this).balance, "Not enough balance for fee value");

if (_isValidSignature(txHash, _transaction.signature)) {
magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC;
}
}

/// @notice Method that should be used to initiate a transaction from this account by an external call.
/// @dev The custom account is supposed to implement this method to initiate a transaction on behalf
/// of the account via L1 -> L2 communication. However, the default account can initiate a transaction
/// from L1, so we formally implement the interface method, but it doesn't execute any logic.
/// @param _transaction The transaction to execute.
function executeTransactionFromOutside(Transaction calldata _transaction) external payable override {
// Behave the same as for fallback/receive, just execute nothing, returns nothing
}

/// @notice Inner method for executing a transaction.
/// @param _transaction The transaction to execute.
function _execute(Transaction calldata _transaction) internal {
address to = address(uint160(_transaction.to));
uint128 value = Utils.safeCastToU128(_transaction.value);
bytes calldata data = _transaction.data;
uint32 gas = Utils.safeCastToU32(gasleft());

// Note, that the deployment method from the deployer contract can only be called with a "systemCall" flag.
bool isSystemCall;
if (to == address(DEPLOYER_SYSTEM_CONTRACT) && data.length >= 4) {
bytes4 selector = bytes4(data[:4]);
// Check that called function is the deployment method,
// the others deployer method is not supposed to be called from the default account.
isSystemCall =
selector == DEPLOYER_SYSTEM_CONTRACT.create.selector ||
selector == DEPLOYER_SYSTEM_CONTRACT.create2.selector ||
selector == DEPLOYER_SYSTEM_CONTRACT.createAccount.selector ||
selector == DEPLOYER_SYSTEM_CONTRACT.create2Account.selector;
}
bool success = EfficientCall.rawCall(gas, to, value, data, isSystemCall);
if (!success) {
EfficientCall.propagateRevert();
}
}

/// @notice Validation that the ECDSA signature of the transaction is correct.
/// @param _hash The hash of the transaction to be signed.
/// @param _signature The signature of the transaction.
/// @return EIP1271_SUCCESS_RETURN_VALUE if the signaure is correct. It reverts otherwise.
function _isValidSignature(bytes32 _hash, bytes memory _signature) internal view returns (bool) {
require(_signature.length == 65, "Signature length is incorrect");
uint8 v;
bytes32 r;
bytes32 s;
assembly {
r := mload(add(_signature, 0x20))
s := mload(add(_signature, 0x40))
v := and(mload(add(_signature, 0x41)), 0xff)
}
require(v == 27 || v == 28, "v is neither 27 nor 28");

require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "Invalid s");

address recoveredAddress = ecrecover(_hash, v, r, s);

return recoveredAddress == owner && recoveredAddress != address(0);
}

fallback() external payable ignoreInDelegateCall {
// fallback of default account shouldn't be called by bootloader under no circumstances
assert(msg.sender != BOOTLOADER_FORMAL_ADDRESS);

// If the contract is called directly, behave like an EOA
}
}
42 changes: 42 additions & 0 deletions contracts/PensionAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 1,42 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";
import "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol";

contract PensionAccountFactory {
bytes32 public pensionAccountBytecodeHash;

constructor(bytes32 _pensionAccountBytecodeHash) {
pensionAccountBytecodeHash = _pensionAccountBytecodeHash;
}

function deployPensionAccount(
bytes32 salt,
address owner,
address dex,
address doge,
address pepe,
address shib,
address btc
) external returns (address pensionAccountAddress) {
(bool success, bytes memory returnData) = SystemContractsCaller
.systemCallWithReturndata(
uint32(gasleft()),
address(DEPLOYER_SYSTEM_CONTRACT),
uint128(0),
abi.encodeCall(
DEPLOYER_SYSTEM_CONTRACT.create2Account,
(
salt,
pensionAccountBytecodeHash,
abi.encode(owner, dex, doge, pepe, shib, btc),
IContractDeployer.AccountAbstractionVersion.Version1
)
)
);
require(success, "Deployment failed");

(pensionAccountAddress) = abi.decode(returnData, (address));
}
}
43 changes: 38 additions & 5 deletions test/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 1,9 @@
import "@matterlabs/hardhat-zksync-node/dist/type-extensions";
import { expect } from "chai";
import * as hre from "hardhat";
import { BigNumber, ethers } from "ethers";
import * as zks from "zksync-web3";
import { deployFactory, deployMultisig, fundAccount } from "./utils";
import { deployFactory, deployMultisig, fundAccount, MultiSigWallet, signMultiSigTx } from "./utils";

const config = {
firstWalletPrivateKey: "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110",
Expand All @@ -14,11 15,13 @@ describe("Account Abstraction Tests", function () {
let accountContract: zks.Contract;
let factoryContractName: string;
let factoryContract: zks.Contract;
let provider: zks.Provider;
let firstRichWallet: zks.Wallet;
let result: any;

before(async function () {
firstRichWallet = new zks.Wallet(config.firstWalletPrivateKey, new zks.Provider(hre.network.config.url));
provider = new zks.Provider(hre.network.config.url);
firstRichWallet = new zks.Wallet(config.firstWalletPrivateKey, provider);
});

describe("Old Example Account Abstraction Tests", function () {
Expand All @@ -36,9 39,15 @@ describe("Account Abstraction Tests", function () {
});

describe("OldAccount", async function () {
let ownerWallet1: zks.Wallet;
let ownerWallet2: zks.Wallet;
let multiSigWallet: MultiSigWallet;
before(async function () {
accountContract = await deployMultisig(firstRichWallet, factoryContract.address);
ownerWallet1 = zks.Wallet.createRandom();
ownerWallet2 = zks.Wallet.createRandom();
accountContract = await deployMultisig(firstRichWallet, factoryContract.address, ownerWallet1, ownerWallet2);
await fundAccount(firstRichWallet, accountContract.address);
await signMultiSigTx(firstRichWallet, accountContract.address, factoryContract.address, ownerWallet1, ownerWallet2);
});

it("Should have a tx hash that starts from 0x", async function () {
Expand All @@ -47,8 56,32 @@ describe("Account Abstraction Tests", function () {
});

it("Should have a balance", async function () {
result = await accountContract.provider.getBalance(accountContract.address);
expect(result).to.not.equal(0);
const result = await accountContract.provider.getBalance(accountContract.address);
// Convert BigNumber to a primitive number for comparison
const balance = parseFloat(ethers.utils.formatEther(result));
expect(balance).to.be.greaterThan(99.99);
});

it("Should be able to send 10 ETH to the main wallet", async function () {
multiSigWallet = new MultiSigWallet(
accountContract.address,
ownerWallet1.privateKey,
ownerWallet2.privateKey,
provider
);
const balanceBefore = (await provider.getBalance(firstRichWallet.address)).toBigInt();
await (
await multiSigWallet.transfer({
to: firstRichWallet.address,
amount: ethers.utils.parseUnits("10", 18),
overrides: { type: 113 },
})
).wait();
const balance = (await provider.getBalance(firstRichWallet.address)).toBigInt();
const difference = balanceBefore - balance;
// expect to be slightly higher than 5
expect(difference / BigInt(10 ** 18) > 4.9).to.be.true;
expect(difference / BigInt(10 ** 18) < 5.1).to.be.true;
});
});
});
Expand Down
Loading

0 comments on commit 0845045

Please sign in to comment.