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
6 changes: 6 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
with `buildAccountSigner`
- A new optional field `createPlt` to `AuthorizationsV1` which exposes the access structure for PLT creation.

## 10.0.0-alpha.13

### Changed

- Disable `denyList`/`allowList` validation on plt token transfers.

## 10.0.0-alpha.12

### Breaking changes
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@concordium/web-sdk",
"version": "10.0.0-alpha.12",
"version": "10.0.0-alpha.13",
"license": "Apache-2.0",
"engines": {
"node": ">=16"
Expand Down
44 changes: 23 additions & 21 deletions packages/sdk/src/plt/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
TokenAddDenyListOperation,
TokenBurnOperation,
TokenMintOperation,
TokenModuleAccountState,
TokenModuleState,
TokenOperation,
TokenOperationType,
Expand Down Expand Up @@ -375,26 +374,29 @@ export async function validateTransfer(
throw new InsufficientFundsError(sender, TokenAmount.fromDecimal(payloadTotal, decimals));
}

if (!token.moduleState.allowList && !token.moduleState.denyList) {
// If the token neither has a deny list nor allow list, we can skip the check.
return true;
}

// Check that sender and all receivers are NOT on the deny list (if present), or that they are included in the allow list (if present).
const receiverInfos = await Promise.all(payloads.map((p) => token.grpc.getAccountInfo(p.recipient.address)));
const accounts = [senderInfo, ...receiverInfos];
accounts.forEach((r) => {
const accountToken = r.accountTokens.find((t) => t.id.value === token.info.id.value)?.state;
const accountModuleState =
accountToken?.moduleState === undefined
? undefined
: (Cbor.decode(accountToken.moduleState) as TokenModuleAccountState);

if (token.moduleState.denyList && accountModuleState?.denyList)
throw new NotAllowedError(TokenHolder.fromAccountAddress(r.accountAddress));
if (token.moduleState.allowList && !accountModuleState?.allowList)
throw new NotAllowedError(TokenHolder.fromAccountAddress(r.accountAddress));
});
// FIXME: This is currently disable as a hacky-fix until some changes in the node are implemented/released.
// https://linear.app/concordium/issue/COR-1650/empty-deny-list-blocking-transfer

// if (!token.moduleState.allowList && !token.moduleState.denyList) {
// // If the token neither has a deny list nor allow list, we can skip the check.
// return true;
// }

// // Check that sender and all receivers are NOT on the deny list (if present), or that they are included in the allow list (if present).
// const receiverInfos = await Promise.all(payloads.map((p) => token.grpc.getAccountInfo(p.recipient.address)));
// const accounts = [senderInfo, ...receiverInfos];
// accounts.forEach((r) => {
// const accountToken = r.accountTokens.find((t) => t.id.value === token.info.id.value)?.state;
// const accountModuleState =
// accountToken?.moduleState === undefined
// ? undefined
// : (Cbor.decode(accountToken.moduleState) as TokenModuleAccountState);

// if (token.moduleState.denyList && accountModuleState?.denyList)
// throw new NotAllowedError(TokenHolder.fromAccountAddress(r.accountAddress));
// if (token.moduleState.allowList && !accountModuleState?.allowList)
// throw new NotAllowedError(TokenHolder.fromAccountAddress(r.accountAddress));
// });

return true;
}
Expand Down
259 changes: 134 additions & 125 deletions packages/sdk/test/ci/plt/Token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,46 +195,49 @@ describe('Token.validateTransfer', () => {
await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.InsufficientFundsError);
});

it('should throw NotAllowedError when account is on deny list', async () => {
const sender = ACCOUNT_1;
const recipient = ACCOUNT_2;
const tokenId = TokenId.fromString('3f1bfce9');
const decimals = 8;

// Setup token with deny list enabled
const moduleState: TokenModuleState = {
name: 'Test Token',
metadata: TokenMetadataUrl.fromString('https://example.com/metadata'),
paused: false,
governanceAccount: TokenHolder.fromAccountAddress(sender),
denyList: true,
};

const token = createMockToken(decimals, moduleState, tokenId);

// Setup sender account with sufficient balance
const senderBalance = TokenAmount.create(BigInt(1000), decimals);

// Create sender account info with deny list status
const senderAccountState: TokenModuleAccountState = { denyList: true };
const senderAccountInfo = createAccountInfo(sender, tokenId, senderBalance, senderAccountState);

// Mock getAccountInfo to return sender on deny list
token.grpc.getAccountInfo = jest
.fn()
.mockResolvedValueOnce(senderAccountInfo) // First call for sender
.mockResolvedValueOnce(createAccountInfo(recipient, tokenId)); // second call for recipient

// Create transfer payload
const transferAmount = TokenAmount.create(BigInt(500), decimals);
const transfer: TokenTransfer = {
amount: transferAmount,
recipient: TokenHolder.fromAccountAddress(recipient),
};

// Should throw NotAllowedError
await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.NotAllowedError);
});
// FIXME: This is currently disable as a hacky-fix until some changes in the node are implemented/released.
// https://linear.app/concordium/issue/COR-1650/empty-deny-list-blocking-transfer

// it('should throw NotAllowedError when account is on deny list', async () => {
// const sender = ACCOUNT_1;
// const recipient = ACCOUNT_2;
// const tokenId = TokenId.fromString('3f1bfce9');
// const decimals = 8;

// // Setup token with deny list enabled
// const moduleState: TokenModuleState = {
// name: 'Test Token',
// metadata: TokenMetadataUrl.fromString('https://example.com/metadata'),
// paused: false,
// governanceAccount: TokenHolder.fromAccountAddress(sender),
// denyList: true,
// };

// const token = createMockToken(decimals, moduleState, tokenId);

// // Setup sender account with sufficient balance
// const senderBalance = TokenAmount.create(BigInt(1000), decimals);

// // Create sender account info with deny list status
// const senderAccountState: TokenModuleAccountState = { denyList: true };
// const senderAccountInfo = createAccountInfo(sender, tokenId, senderBalance, senderAccountState);

// // Mock getAccountInfo to return sender on deny list
// token.grpc.getAccountInfo = jest
// .fn()
// .mockResolvedValueOnce(senderAccountInfo) // First call for sender
// .mockResolvedValueOnce(createAccountInfo(recipient, tokenId)); // second call for recipient

// // Create transfer payload
// const transferAmount = TokenAmount.create(BigInt(500), decimals);
// const transfer: TokenTransfer = {
// amount: transferAmount,
// recipient: TokenHolder.fromAccountAddress(recipient),
// };

// // Should throw NotAllowedError
// await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.NotAllowedError);
// });

it('should validate successfully for tokens with deny list when account does not have a balance', async () => {
const sender = ACCOUNT_1;
Expand Down Expand Up @@ -276,91 +279,97 @@ describe('Token.validateTransfer', () => {
await expect(Token.validateTransfer(token, sender, transfer)).resolves.toBe(true);
});

it('should throw NotAllowedError for tokens with allow list when account does not have a balance', async () => {
const sender = ACCOUNT_1;
const recipient = ACCOUNT_2;
const tokenId = TokenId.fromString('3f1bfce9');
const decimals = 8;

// Setup token with deny list enabled
const moduleState: TokenModuleState = {
name: 'Test Token',
metadata: TokenMetadataUrl.fromString('https://example.com/metadata'),
governanceAccount: TokenHolder.fromAccountAddress(sender),
allowList: true,
paused: false,
};

const token = createMockToken(decimals, moduleState, tokenId);

// Setup sender account with sufficient balance
const senderBalance = TokenAmount.create(BigInt(1000), decimals);

// Create sender account info with deny list status
const senderAccountState: TokenModuleAccountState = { allowList: true };
const senderAccountInfo = createAccountInfo(sender, tokenId, senderBalance, senderAccountState);

// Mock getAccountInfo to return sender on deny list
token.grpc.getAccountInfo = jest
.fn()
.mockResolvedValueOnce(senderAccountInfo) // First call for balance check
.mockResolvedValueOnce(createAccountInfo(recipient)); // second call for recipient, no token balance.

// Create transfer payload
const transferAmount = TokenAmount.create(BigInt(500), decimals);
const transfer: TokenTransfer = {
amount: transferAmount,
recipient: TokenHolder.fromAccountAddress(recipient),
};

// Should throw NotAllowedError
await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.NotAllowedError);
});

it('should throw NotAllowedError when account is not on allow list', async () => {
const sender = ACCOUNT_1;
const recipient = ACCOUNT_2;
const tokenId = TokenId.fromString('3f1bfce9');
const decimals = 8;

// Setup token with allow list enabled
const moduleState: TokenModuleState = {
name: 'Test Token',
metadata: TokenMetadataUrl.fromString('https://example.com/metadata'),
paused: false,
governanceAccount: TokenHolder.fromAccountAddress(sender),
allowList: true,
};

const token = createMockToken(decimals, moduleState, tokenId);

// Setup sender account with sufficient balance
const senderBalance = TokenAmount.create(BigInt(1000), decimals);

// Create sender account info with allow list status set to true
const senderAccountState: TokenModuleAccountState = { allowList: true };
const senderAccountInfo = createAccountInfo(sender, tokenId, senderBalance, senderAccountState);

// Create recipient account info with allow list status set to true
const recipientAccountState: TokenModuleAccountState = { allowList: false };
const recipientAccountInfo = createAccountInfo(recipient, tokenId, undefined, recipientAccountState);

// Mock getAccountInfo to return sender not on allow list
token.grpc.getAccountInfo = jest
.fn()
.mockResolvedValueOnce(senderAccountInfo) // First call for sender
.mockResolvedValueOnce(recipientAccountInfo); // second call for recipient

// Create transfer payload
const transferAmount = TokenAmount.create(BigInt(500), decimals);
const transfer: TokenTransfer = {
amount: transferAmount,
recipient: TokenHolder.fromAccountAddress(recipient),
};

// Should throw NotAllowedError
await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.NotAllowedError);
});
// FIXME: This is currently disable as a hacky-fix until some changes in the node are implemented/released.
// https://linear.app/concordium/issue/COR-1650/empty-deny-list-blocking-transfer

// it('should throw NotAllowedError for tokens with allow list when account does not have a balance', async () => {
// const sender = ACCOUNT_1;
// const recipient = ACCOUNT_2;
// const tokenId = TokenId.fromString('3f1bfce9');
// const decimals = 8;

// // Setup token with deny list enabled
// const moduleState: TokenModuleState = {
// name: 'Test Token',
// metadata: TokenMetadataUrl.fromString('https://example.com/metadata'),
// governanceAccount: TokenHolder.fromAccountAddress(sender),
// allowList: true,
// paused: false,
// };

// const token = createMockToken(decimals, moduleState, tokenId);

// // Setup sender account with sufficient balance
// const senderBalance = TokenAmount.create(BigInt(1000), decimals);

// // Create sender account info with deny list status
// const senderAccountState: TokenModuleAccountState = { allowList: true };
// const senderAccountInfo = createAccountInfo(sender, tokenId, senderBalance, senderAccountState);

// // Mock getAccountInfo to return sender on deny list
// token.grpc.getAccountInfo = jest
// .fn()
// .mockResolvedValueOnce(senderAccountInfo) // First call for balance check
// .mockResolvedValueOnce(createAccountInfo(recipient)); // second call for recipient, no token balance.

// // Create transfer payload
// const transferAmount = TokenAmount.create(BigInt(500), decimals);
// const transfer: TokenTransfer = {
// amount: transferAmount,
// recipient: TokenHolder.fromAccountAddress(recipient),
// };

// // Should throw NotAllowedError
// await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.NotAllowedError);
// });

// FIXME: This is currently disable as a hacky-fix until some changes in the node are implemented/released.
// https://linear.app/concordium/issue/COR-1650/empty-deny-list-blocking-transfer

// it('should throw NotAllowedError when account is not on allow list', async () => {
// const sender = ACCOUNT_1;
// const recipient = ACCOUNT_2;
// const tokenId = TokenId.fromString('3f1bfce9');
// const decimals = 8;

// // Setup token with allow list enabled
// const moduleState: TokenModuleState = {
// name: 'Test Token',
// metadata: TokenMetadataUrl.fromString('https://example.com/metadata'),
// paused: false,
// governanceAccount: TokenHolder.fromAccountAddress(sender),
// allowList: true,
// };

// const token = createMockToken(decimals, moduleState, tokenId);

// // Setup sender account with sufficient balance
// const senderBalance = TokenAmount.create(BigInt(1000), decimals);

// // Create sender account info with allow list status set to true
// const senderAccountState: TokenModuleAccountState = { allowList: true };
// const senderAccountInfo = createAccountInfo(sender, tokenId, senderBalance, senderAccountState);

// // Create recipient account info with allow list status set to true
// const recipientAccountState: TokenModuleAccountState = { allowList: false };
// const recipientAccountInfo = createAccountInfo(recipient, tokenId, undefined, recipientAccountState);

// // Mock getAccountInfo to return sender not on allow list
// token.grpc.getAccountInfo = jest
// .fn()
// .mockResolvedValueOnce(senderAccountInfo) // First call for sender
// .mockResolvedValueOnce(recipientAccountInfo); // second call for recipient

// // Create transfer payload
// const transferAmount = TokenAmount.create(BigInt(500), decimals);
// const transfer: TokenTransfer = {
// amount: transferAmount,
// recipient: TokenHolder.fromAccountAddress(recipient),
// };

// // Should throw NotAllowedError
// await expect(Token.validateTransfer(token, sender, transfer)).rejects.toThrow(Token.NotAllowedError);
// });

it('should validate batch transfers when total amount is within balance', async () => {
const sender = ACCOUNT_1;
Expand Down