hoodquidity/packages/contracts/lib/openzeppelin-contracts/test/utils/RelayedCall.test.js
Денис Зефиров dba2811447 Hoodquidity: liquidity campaigns for Robinhood Chain stock token markets
Full build: Vite/React acid-terminal frontend (BEARER design system,
Panchang/Sentient/Fragment Mono, neon layer, palette audit script),
HoodquidityVaults Foundry contracts (single+dual deposits via Chainlink-style
feeds, hybrid exit, 10% platform fee, boost, Genesis LP), Reown AppKit wallet
connect with Robinhood Chain testnet (46630), live data from Dexscreener and
onchain reads, partner portal, design-sync setup for claude.ai/design.

Deploy: static via Forgejo Actions platform, BUILD_DIR=apps/web/dist (.env).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 08:32:28 +04:00

223 lines
8.2 KiB
JavaScript

const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { impersonate } = require('../helpers/account');
async function fixture() {
const [admin, receiver, other] = await ethers.getSigners();
const mock = await ethers.deployContract('$RelayedCall');
const computeRelayerAddress = (salt = ethers.ZeroHash) =>
ethers.getCreate2Address(
mock.target,
salt,
ethers.keccak256(
ethers.concat([
'0x60475f8160095f39f373',
mock.target,
'0x331460133611166022575f5ffd5b6014360360145f375f5f601436035f345f3560601c5af13d5f5f3e5f3d91604557fd5bf3',
]),
),
);
const authority = await ethers.deployContract('$AccessManager', [admin]);
const target = await ethers.deployContract('$AccessManagedTarget', [authority]);
return { mock, target, receiver, other, computeRelayerAddress };
}
describe('RelayedCall', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
describe('default (zero) salt', function () {
beforeEach(async function () {
this.relayer = await this.computeRelayerAddress();
});
it('automatic relayer deployment', async function () {
await expect(ethers.provider.getCode(this.relayer)).to.eventually.equal('0x');
// First call performs deployment
await expect(this.mock.$getRelayer()).to.emit(this.mock, 'return$getRelayer').withArgs(this.relayer);
await expect(ethers.provider.getCode(this.relayer)).to.eventually.not.equal('0x');
// Following calls use the same relayer
await expect(this.mock.$getRelayer()).to.emit(this.mock, 'return$getRelayer').withArgs(this.relayer);
});
describe('relayed call', function () {
it('target success', async function () {
const tx = this.mock.$relayCall(
ethers.Typed.address(this.target),
ethers.Typed.bytes(this.target.interface.encodeFunctionData('fnUnrestricted', [])),
);
await expect(tx)
.to.emit(this.target, 'CalledUnrestricted')
.withArgs(this.relayer)
.to.emit(this.mock, 'return$relayCall_address_bytes')
.withArgs(true, '0x');
});
it('target success (with value)', async function () {
const value = 42n;
// fund the mock
await this.other.sendTransaction({ to: this.mock.target, value });
// perform relayed call
const tx = this.mock.$relayCall(
ethers.Typed.address(this.receiver),
ethers.Typed.uint256(value),
ethers.Typed.bytes('0x'),
);
await expect(tx).to.changeEtherBalances([this.mock, this.relayer, this.receiver], [-value, 0n, value]);
await expect(tx).to.emit(this.mock, 'return$relayCall_address_uint256_bytes').withArgs(true, '0x');
});
it('target revert', async function () {
const tx = this.mock.$relayCall(
ethers.Typed.address(this.target),
ethers.Typed.bytes(this.target.interface.encodeFunctionData('fnRestricted', [])),
);
await expect(tx)
.to.emit(this.mock, 'return$relayCall_address_bytes')
.withArgs(false, this.target.interface.encodeErrorResult('AccessManagedUnauthorized', [this.relayer]));
});
});
it('direct call to the relayer', async function () {
// deploy relayer
await this.mock.$getRelayer();
// unauthorized caller
await expect(
this.other.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce525' }),
).to.be.revertedWithoutReason();
});
it('relayer input format', async function () {
// deploy relayer
await this.mock.$getRelayer();
// impersonate mock to pass caller checks
const mockAsWallet = await impersonate(this.mock.target);
// 20 bytes (address + empty data) - OK
await expect(
mockAsWallet.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce525' }),
).to.not.be.reverted;
// 19 bytes (not enough for an address) - REVERT
await expect(
mockAsWallet.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce5' }),
).to.be.revertedWithoutReason();
// 0 bytes (not enough for an address) - REVERT
await expect(mockAsWallet.sendTransaction({ to: this.relayer, data: '0x' })).to.be.revertedWithoutReason();
});
});
describe('random salt', function () {
beforeEach(async function () {
this.salt = ethers.hexlify(ethers.randomBytes(32));
this.relayer = await this.computeRelayerAddress(this.salt);
});
it('automatic relayer deployment', async function () {
await expect(ethers.provider.getCode(this.relayer)).to.eventually.equal('0x');
// First call performs deployment
await expect(this.mock.$getRelayer(ethers.Typed.bytes32(this.salt)))
.to.emit(this.mock, 'return$getRelayer_bytes32')
.withArgs(this.relayer);
await expect(ethers.provider.getCode(this.relayer)).to.eventually.not.equal('0x');
// Following calls use the same relayer
await expect(this.mock.$getRelayer(ethers.Typed.bytes32(this.salt)))
.to.emit(this.mock, 'return$getRelayer_bytes32')
.withArgs(this.relayer);
});
describe('relayed call', function () {
it('target success', async function () {
const tx = this.mock.$relayCall(
ethers.Typed.address(this.target),
ethers.Typed.bytes(this.target.interface.encodeFunctionData('fnUnrestricted', [])),
ethers.Typed.bytes32(this.salt),
);
await expect(tx)
.to.emit(this.target, 'CalledUnrestricted')
.withArgs(this.relayer)
.to.emit(this.mock, 'return$relayCall_address_bytes_bytes32')
.withArgs(true, '0x');
});
it('target success (with value)', async function () {
const value = 42n;
// fund the mock
await this.other.sendTransaction({ to: this.mock.target, value });
// perform relayed call
const tx = this.mock.$relayCall(
ethers.Typed.address(this.receiver),
ethers.Typed.uint256(value),
ethers.Typed.bytes('0x'),
ethers.Typed.bytes32(this.salt),
);
await expect(tx).to.changeEtherBalances([this.mock, this.relayer, this.receiver], [-value, 0n, value]);
await expect(tx).to.emit(this.mock, 'return$relayCall_address_uint256_bytes_bytes32').withArgs(true, '0x');
});
it('target revert', async function () {
const tx = this.mock.$relayCall(
ethers.Typed.address(this.target),
ethers.Typed.bytes(this.target.interface.encodeFunctionData('fnRestricted', [])),
ethers.Typed.bytes32(this.salt),
);
await expect(tx)
.to.emit(this.mock, 'return$relayCall_address_bytes_bytes32')
.withArgs(false, this.target.interface.encodeErrorResult('AccessManagedUnauthorized', [this.relayer]));
});
});
it('direct call to the relayer', async function () {
// deploy relayer
await this.mock.$getRelayer(ethers.Typed.bytes32(this.salt));
// unauthorized caller
await expect(
this.other.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce525' }),
).to.be.revertedWithoutReason();
});
it('input format', async function () {
// deploy relayer
await this.mock.$getRelayer(ethers.Typed.bytes32(this.salt));
// impersonate mock to pass caller checks
const mockAsWallet = await impersonate(this.mock.target);
// 20 bytes (address + empty data) - OK
await expect(
mockAsWallet.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce525' }),
).to.not.be.reverted;
// 19 bytes (not enough for an address) - REVERT
await expect(
mockAsWallet.sendTransaction({ to: this.relayer, data: '0x7859821024E633C5dC8a4FcF86fC52e7720Ce5' }),
).to.be.revertedWithoutReason();
// 0 bytes (not enough for an address) - REVERT
await expect(mockAsWallet.sendTransaction({ to: this.relayer, data: '0x' })).to.be.revertedWithoutReason();
});
});
});