Skip to content
Merged
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
10 changes: 5 additions & 5 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ POST /auth/token {grant_type=authorization_code, code, code_verifier, …}

| Field | Description | History kind type(s) |
| --- | --- | --- |
| `kind` | Entry type. Known values are `deposit`, `withdraw`, `createLock`, `transferFromLock`, and `transferBalance`; undecoded entries return `unknown`. | all |
| `kind` | Entry type. Known values are `deposit`, `withdraw`, `createLock`, `transferFromLockOut`, `transferFromLockIn`, `transferBalanceOut`, `transferBalanceIn`, `modifyLock`, and `unlockLock`; undecoded entries return `unknown`. | all |
| `timestamp` | Entry timestamp. | all |
| `token_id` | Token identifier. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
| `amount` | Token amount as a decimal string. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
| `chain_id` | Source chain for `token_id`, when known. | `deposit`, `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
| `token_id` | Token identifier. | all decoded kinds |
| `amount` | Token amount as a decimal string. For `modifyLock`, this is the additional locked amount and can be `0` for expiry-only changes. For `unlockLock`, this is the amount returned to available balance. | all decoded kinds |
| `chain_id` | Source chain for `token_id`, when known. | all decoded kinds |
| `deposit_id` | Deposit identifier. | `deposit` |
| `counterparty` | Address payload for non-deposit entries: withdrawal destination, lock service, or transfer recipient. | `withdraw`, `createLock`, `transferFromLock`, `transferBalance` |
| `counterparty` | Address payload for non-deposit entries: withdrawal destination for `withdraw`, lock service for `createLock`/`modifyLock`/`unlockLock`, the recipient for `transferFromLockOut`/`transferBalanceOut`, and the sender for `transferFromLockIn`/`transferBalanceIn`. | all decoded kinds except `deposit` |

## Deposit Flow

Expand Down
8 changes: 6 additions & 2 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,12 @@
"deposit",
"withdraw",
"createLock",
"transferFromLock",
"transferBalance",
"transferFromLockOut",
"transferFromLockIn",
"transferBalanceOut",
"transferBalanceIn",
"modifyLock",
"unlockLock",
"unknown"
],
"title": "Kind",
Expand Down
96 changes: 84 additions & 12 deletions solidity/contracts/Accounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,29 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
);
}

/**
* @dev Appends a single history entry for `user` packing the token, amount,
* and counterparty.
* @param user The account whose history is appended.
* @param kind The history entry kind.
* @param tokenId The token involved in the operation.
* @param amount The operation amount.
* @param counterparty The other party to the operation.
*/
function _appendUserCounterpartyHistory(
address user,
HistoryKind kind,
bytes32 tokenId,
uint256 amount,
address counterparty
) internal {
_appendHistory(
user,
kind,
abi.encodePacked(tokenId, amount, counterparty)
);
}

/**
* @notice Get the deposit address for an authenticated user.
* @param chainType The chain family (see ChainType enum)
Expand Down Expand Up @@ -403,10 +426,12 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
})
);

_appendHistory(
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.CreateLock,
abi.encodePacked(tokenId, amount, serviceAddress)
tokenId,
amount,
serviceAddress
);
}

Expand Down Expand Up @@ -508,6 +533,13 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
}

lock.expiry = newExpiry;
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.ModifyLock,
lock.tokenId,
amount,
lock.serviceId
);
}

/**
Expand Down Expand Up @@ -547,6 +579,13 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
if (lock.amount != 0) {
if (block.timestamp < lock.expiry) revert LockNotExpired();
balances[userAddress][lock.tokenId] += lock.amount;
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.UnlockLock,
lock.tokenId,
lock.amount,
lock.serviceId
);
}

locks[lockIndex] = locks[locks.length - 1];
Expand Down Expand Up @@ -581,6 +620,13 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad

if (block.timestamp >= lock.expiry && lock.amount > 0) {
balances[userAddress][lock.tokenId] += lock.amount;
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.UnlockLock,
lock.tokenId,
lock.amount,
lock.serviceId
);

locks[i] = locks[locks.length - 1];
locks.pop();
Expand Down Expand Up @@ -655,11 +701,22 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
lock.amount -= amount;
balances[toAddress][lock.tokenId] += amount;

_appendHistory(
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.TransferFromLock,
abi.encodePacked(lock.tokenId, amount, toAddress)
HistoryKind.TransferFromLockOut,
lock.tokenId,
amount,
toAddress
);
if (toAddress != address(0) && toAddress != userAddress) {
_appendUserCounterpartyHistory(
toAddress,
HistoryKind.TransferFromLockIn,
lock.tokenId,
amount,
userAddress
);
}

if (lock.amount == 0) {
locks[lockIndex] = locks[locks.length - 1];
Expand Down Expand Up @@ -719,10 +776,12 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
}

_scheduleWithdrawal(userAddress, toAddress, tokenId, amount);
_appendHistory(
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.Withdraw,
abi.encodePacked(tokenId, amount, toAddress)
tokenId,
amount,
toAddress
);
}

Expand Down Expand Up @@ -775,11 +834,22 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
balances[userAddress][tokenId] -= amount;
balances[toAddress][tokenId] += amount;

_appendHistory(
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.TransferBalance,
abi.encodePacked(tokenId, amount, toAddress)
HistoryKind.TransferBalanceOut,
tokenId,
amount,
toAddress
);
if (toAddress != address(0) && toAddress != userAddress) {
_appendUserCounterpartyHistory(
toAddress,
HistoryKind.TransferBalanceIn,
tokenId,
amount,
userAddress
);
}
}

/**
Expand Down Expand Up @@ -821,10 +891,12 @@ contract Accounting is EIP712SignatureVerifier, EVMSignerAndVerifier, UUPSUpgrad
balances[userAddress][tokenId] -= amount;

_scheduleWithdrawal(userAddress, userAddress, tokenId, amount);
_appendHistory(
_appendUserCounterpartyHistory(
userAddress,
HistoryKind.Withdraw,
abi.encodePacked(tokenId, amount, userAddress)
tokenId,
amount,
userAddress
);
}

Expand Down
12 changes: 10 additions & 2 deletions solidity/contracts/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,20 @@ enum ChainType {
/// or references a tokenId that has not been registered via registerToken.
error UnsupportedTokenType();

/// @notice User-visible history entry kinds.
/// @dev Ordinals are the wire encoding shared with Python `HistoryKind`; keep
/// them in lockstep. Before durable deployments they may be renumbered
/// with a coordinated redeploy; afterwards preserve existing ordinals.
enum HistoryKind {
Deposit,
Withdraw,
CreateLock,
TransferFromLock,
TransferBalance
TransferFromLockOut,
TransferFromLockIn,
TransferBalanceOut,
TransferBalanceIn,
ModifyLock,
UnlockLock
}

struct TokenInfo {
Expand Down
15 changes: 15 additions & 0 deletions solidity/contracts/test/MockAccounting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,19 @@ contract MockAccounting is Accounting {
roflSignerAddress = newSigner;
emit RoflSignerUpdated(newSigner);
}

/**
* @notice Calls mockCreditDeposit n times from solidity to speed up the sapphire-localnet tests.
*/
function mockCreditDepositNTimes(
address beneficiary,
bytes32 tokenId,
uint256 amount,
bytes32 depositId,
uint256 n
) external {
for (uint256 i = 0; i < n; i++) {
this.mockCreditDeposit(beneficiary, tokenId, i + amount, keccak256(abi.encodePacked(depositId, i)));
}
}
}
31 changes: 0 additions & 31 deletions solidity/contracts/test/MockAccountingHelper.sol

This file was deleted.

7 changes: 6 additions & 1 deletion solidity/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const config: HardhatUserConfig = {
},
hardhat: {
accounts: TEST_HDWALLET,
// Accounting may exceed the EIP-170 24576-byte cap; Sapphire allows 64 KiB
// (see scripts/check-bytecode-size.ts), so lift the cap on the in-process
// test network too.
allowUnlimitedContractSize: true,
}
},
sourcify: {
Expand All @@ -50,7 +54,8 @@ const config: HardhatUserConfig = {
evmVersion: 'paris',
optimizer: {
enabled: true,
// Keep bytecode size below EIP-170 limits; large "runs" can bloat size significantly.
// Keep bytecode size within the Sapphire 64 KiB budget enforced by
// scripts/check-bytecode-size.ts; large "runs" values bloat size significantly.
runs: 20,
},
viaIR: true,
Expand Down
10 changes: 7 additions & 3 deletions solidity/scripts/check-bytecode-size.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { artifacts } from 'hardhat';

const EIP170_LIMIT_BYTES = 24576;
// Accounting deploys to Oasis Sapphire, whose runtime raised the create-contract
// limit to 64 KiB (oasis-sdk#2471, Sapphire >= 1.3.0-testnet). Keep a 1 KiB
// safety buffer so nothing is ever deployed right at the hard ceiling.
const SAPPHIRE_LIMIT_BYTES = 65536;
const SIZE_BUFFER_BYTES = 1024;

async function checkContractSize(contractName: string, maxBytes: number): Promise<void> {
const artifact = await artifacts.readArtifact(contractName);
Expand All @@ -13,13 +17,13 @@ async function checkContractSize(contractName: string, maxBytes: number): Promis

if (deployedSize > maxBytes) {
throw new Error(
`${contractName} exceeds EIP-170 limit by ${deployedSize - maxBytes} bytes (${deployedSize}/${maxBytes}).`
`${contractName} exceeds the Sapphire contract size budget by ${deployedSize - maxBytes} bytes (${deployedSize}/${maxBytes}).`
);
}
}

async function main() {
await checkContractSize('Accounting', EIP170_LIMIT_BYTES);
await checkContractSize('Accounting', SAPPHIRE_LIMIT_BYTES - SIZE_BUFFER_BYTES);
console.log('Bytecode size checks passed.');
}

Expand Down
16 changes: 10 additions & 6 deletions solidity/test/Accounting.E2E.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { keccak256, Wallet } from 'ethers';
import { MockAccounting, MockAccountingV2, MockSiweAuth } from '../typechain-types';
import { HardhatNetworkHDAccountsConfig } from 'hardhat/types';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { deployMockAccounting, getDeployer, MOCK_ROFL_APP_ID, mockAuthToken } from './utils';
import { deployMockAccounting, getDeployer, MOCK_ROFL_APP_ID, mockAuthToken, waitForImplementationChange } from './utils';

// Mirrors of the Solidity enums in contracts/Types.sol. Typechain exposes enum
// parameters as uint8 at the TS boundary, so we use ordinals — kept in sync with
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('Accounting', function () {
let userWallet2: Wallet;
let tokenId: string;

before(async () => {
before(async function () {
const [user1, user2, service] = (await ethers.getSigners()).slice(1, 4);
const deployer = getDeployer();

Expand Down Expand Up @@ -792,7 +792,7 @@ describe('WithdrawFromLock', function () {
.connect(provider) as any;
});

beforeEach(async () => {
beforeEach(async function () {
const [deployer] = await ethers.getSigners();

const MockSiweAuthFactory = await ethers.getContractFactory('MockSiweAuth');
Expand Down Expand Up @@ -991,7 +991,7 @@ describe('ModifyLock', function () {

const MOCK_ROFL_APP_ID = "0x" + "00".repeat(21);

before(async () => {
before(async function () {
const provider = ethers.provider;
const [user1, user2] = (await ethers.getSigners()).slice(1,3);
const deployer = getDeployer();
Expand Down Expand Up @@ -1410,7 +1410,7 @@ describe('Upgradability', function () {

const MOCK_ROFL_APP_ID = "0x" + "00".repeat(21);

before(async () => {
before(async function () {
const deployer = getDeployer();

const MockSiweAuthFactory = await ethers.getContractFactory('MockSiweAuth', deployer);
Expand Down Expand Up @@ -1572,15 +1572,19 @@ describe('Upgradability', function () {
expect(balanceBefore).to.equal(initialBalance);

// Upgrade to V2 (reinitializer doesn't chain parent inits — they ran in V1)
const implementationBefore = await upgrades.erc1967.getImplementationAddress(proxyAddress);
const AccountingV2Factory = await ethers.getContractFactory('MockAccountingV2');
const upgraded = await upgrades.upgradeProxy(proxyAddress, AccountingV2Factory, {
kind: 'uups',
unsafeAllow: ['missing-initializer'],
constructorArgs: [await mockSiweAuth.getAddress()],
}) as unknown as MockAccountingV2;

// sapphire-paratime#688: upgradeProxy may return before the upgrade tx lands.
await waitForImplementationChange(proxyAddress, implementationBefore);

// Call reinitializer
await upgraded.initializeV2(42);
await (await upgraded.initializeV2(42)).wait();

// Verify new state is set
expect(await upgraded.newStateVar()).to.equal(42);
Expand Down
Loading
Loading