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>
223 lines
8.2 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|